Webpack版本升级与打包优化

1. 9.0版本

1.1 动态路由修改

  1. 打包分析
Show chunks: All (3.3 MB)
js/app.f6e590defd09f0af0533.js (1.23 MB)
js/0.dcb7a6fef441f8b29aad.js (759.71 KB)
js/1.12b87a568748b35be091.js (753 KB)
js/vendor.3a71817ce04ee0d52abd.js (477.57 KB)
js/2.b41119393c5fed1db5a7.js (50.59 KB)
js/3.1e900085df4e92b118d0.js (44.05 KB)
js/4.cd0ae5676c3c19029aaa.js (30.54 KB)
js/5.43b81f7e8267bd84e89e.js (2.27 KB)
js/6.8d54cf0b69937a8c4ca4.js (1.73 KB)
js/manifest.aab2c6e6e57bd770452c.js (1.58 KB)
image.png

(1) 动态路由生成js文件没有自定义名
(2) 0.js1.js代码几乎都是重复的。
(3) 2.js3.js4.js动态路由生成的代码体积太小。

  1. 设置打包配置chunkFilename
    webpack.config.production.js
    output: {
        path: config.path.dist,
-        filename: 'js/[name].[chunkhash].js',
-        chunkFilename: 'js/[id].[chunkhash].js',
+        filename: 'js/[name].[chunkhash:5].js',
+        chunkFilename: 'js/[name].[chunkhash:5].js',
        publicPath: '?op=resource&encode=utf8&resource=/com/fr/wei/plugin/h5reportnew/dist/'
    },
  1. 自定义chunkName并进行路由合并
    routerConfig.web.js
-import Login from 'bundle-loader?lazy!../../platform/view/Login/'
-import Directory from 'bundle-loader?lazy!../../platform/view/Directory'
-import ReportPage from 'bundle-loader?lazy!../../fr/page/ReportPage'
-import FormPage from 'bundle-loader?lazy!../../fr/page/FormPage'
-import WebPage from 'bundle-loader?lazy!../../platform/view/WebPage'
-import ChangePassword from 'bundle-loader?lazy!../../platform/view/ChangePassword/ChangePassword'
-import CprPage from 'bundle-loader?lazy!../../fr/page/CprPage'
+import Login from 'bundle-loader?lazy&name=platform!../../platform/view/Login/'
+import Directory from 'bundle-loader?lazy&name=platform!../../platform/view/Directory'
+import ReportPage from 'bundle-loader?lazy&name=FRPage!../../fr/page/ReportPage'
+import FormPage from 'bundle-loader?lazy&name=FRPage!../../fr/page/FormPage'
+import WebPage from 'bundle-loader?lazy&name=platform!../../platform/view/WebPage'
+import ChangePassword from 'bundle-loader?lazy&name=platform!../../platform/view/ChangePassword/ChangePassword'
+import CprPage from 'bundle-loader?lazy&name=platform!../../fr/page/CprPage'
  1. 打包分析
Show chunks: All (2.57 MB)
js/app.3027a.js (1.23 MB)
js/FRPage.792bb.js (771.79 KB)
js/vendor.6f983.js (477.57 KB)
js/platform.aee33.js (128.05 KB)
js/manifest.0559a.js (1.46 KB)
image.png

1.2 图表资源提前加载

  1. 图表资源加载分析
    ChartView组件的didMount中请求加载图表资源,导致图表空白1-2s才能渲染出来。
  2. 提前加载图表资源
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/web/core/css/leaflet.css' %>"/>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/mobile/js/appChart.js' %>"></script>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/web/core/js/vancharts-all.js' %>"></script>
  1. 时间对比


    测试数据.png

1.3 打包区分不同入口

  1. 多入口分析
    项目支持多入口,不同入口打开同一个html页面。页面初始加载app.jsvendor.jsmanifest.js资源。当路由跳转到platformFRPage对应页面时,动态加载platform.jsFRPage.js
    当从模板页(cptfrm)进入项目时,FRPage.js仍通过动态路由的方式加载,加载时机有些延迟,增加了页面打开时间(客户反映提升不大)。
  2. 打包生成多个html页面,不同入口打开不同的html页面。当从登录页进入项目时,使用动态路由;当直接打开cpt或者frm模板时,不使用动态路由,提前加载对应js资源。
  3. 打包分析
Show chunks: All (4.55 MB)
js/sync.3bd37d4.js (1.98 MB)
js/app.cf1d197.js (1.23 MB)
js/FRPage.2285cd3.js (770.97 KB)
js/vendor.72c6bc3.js (478.69 KB)
js/platform.d5fb09a.js (128.02 KB)
js/manifest.43d0f75.js (1.46 KB)
image.png

1.4 代码分割

  1. Performance分析图
    有几个图表的表单.png
  2. 生成的app.js体积很大,但CommonsChunkPlugin只适用于多入口文件公共模块代码分割。本项目入口只有一个index.web.js,不符合CommonsChunkPlugin插件的应用场景。
  3. webpack升级
    (1) Cyclic dependency error
    解决:https://github.com/marcelklehr/toposort/issues/20
    (2) 其他见10.0 webpack升级遇到问题及解决方案。
  4. 代码分割
optimization: {
    splitChunks: {
        chunks: 'all',
        maxInitialRequests: 6,
        automaticNameDelimiter: '-',
        cacheGroups: {
            //base代码分割
            baseComponent: {
                test: /[\\/]public[\\/]base[\\/]components/,
                priority: 5,
                name: 'baseComponent'
            },
            appBase: {
                test: /[\\/]public[\\/]base/,
                priority: 0,
                name: 'appBase'
            },
            //react
            react: {
                test: /[\\/]node_modules[\\/](react\.*|redux\.*)|[\\/]lib[\\/]reactweb/,
                priority: -5,
                name: 'react',
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                name: 'vendor'
            }
        }
    }
}
  1. 时间对比


    测试数据

2. 10.0版本打包优化

2.1 知识扩展

  1. babel-node 命令
  2. minimist 轻量级的命令行参数解析引擎
  3. define-plugin

2.2 问题分析

  1. happypack报错
    Cannot read property 'length' of undefined
if (resolve.length === 4) {
                ^
TypeError: Cannot read property 'length' of undefined
    at resolveLoader (...\node_modules\happypack\lib\WebpackUtils.js:138:17)
    at ...\node_modules\happypack\lib\WebpackUtils.js:126:7
    at ...\node_modules\happypack\node_modules\async\lib\async.js:713:13

分析:happypack 4.X不兼容webpack 4.X
解决:升级happypack5.X版本。

  1. file-loader报错
    Cannot read property 'fileLoader' of undefined
ERROR in ./node_modules/rs-styles/images/sampleStamp.png
Module build failed: TypeError: Cannot read property 'fileLoader' of undefined
    at Object.module.exports (/Users/uri/Documents/connect/dashboard/node_modules/file-loader/index.js:14:28)
    at Object.module.exports (/Users/uri/Documents/connect/dashboard/node_modules/url-loader/index.js:31:23)

分析:url-loader使用了旧版本的file-loader
解决:更新升级file-loader版本。
参考:https://github.com/webpack/webpack/issues/6419

  1. file-loader报错
    file-loader fails with JSON files in Webpack 4
Module parse failed: Unexpected token m in JSON at position 0
You may need an appropriate loader to handle this file type.
SyntaxError: Unexpected token m in JSON at position 0
    at JSON.parse (<anonymous>)
    at JsonParser.parse (/Users/jeremy/Documents/Development/webpack-file-loader-test/node_modules/webpack/lib/JsonParser.js:15:21)

分析:在webpack4.X版本中,默认支持json文件,无需loader处理。
解决:删除json处理的loader({test: /\.json$/,use: 'json-loader'})。
参考:https://github.com/webpack-contrib/file-loader/issues/259
4. 动态import()报错(☆☆)

ERROR in ./pages/Home/index.tsx 5:16
Module parse failed: Unexpected token (5:16)
You may need an appropriate loader to handle this file type.
| import { BeatLoader } from 'react-spinners';
| export const LoadableHomePage = Loadable({
>   loader: () => import(
|   /* webpackChunkName: "homepage" */
|   './page'),

方法一:npm update acorn --depth 20npm dedupe、删除node_modules、删除package.lock.jsonnpm install
方法二:npm install webpack@4.28.4
参考:https://github.com/webpack/webpack/issues/8656

  1. uglifyjs-webpack-plugin压缩动态import()语法报错
    分析:uglifyjs-webpack-plugin只支持ES5代码压缩,不支持ES6代码压缩。
    解决:使用terser-webpack-plugin替换掉uglifyjs-webpack-plugin
    参考:https://webpack.docschina.org/plugins/terser-webpack-plugin
  2. import()动态加载魔法注释webpackChunkName失效
    分析:①在项目中使用了module:metro-react-native-babel-preset,该presetbabelReactNative应用提供的,ReactNative应用默认使用它转化代码。②该preset支持动态import(),无需再使用@babel/plugin-syntax-dynamic-import。③ 该preset会在babel打包过程中删除注释内容。
    image.png

    解决:babel配置中添加comments: true
    参考:https://github.com/webpack/webpack/issues/4861
    https://babeljs.io/docs/en/options#comments
    https://www.npmjs.com/package/metro-react-native-babel-preset
  3. 打包生成的bundle中含有动态import()之外的js文件
    image.png

    分析:optimization.splitChunks在进行代码分割时,会默认将不同chunk引入的相同modules进行分割,避免这些代码重复打包到不同的bundle
    解决:配置optimization.splitChunks中的cacheGroups,将共用代码提取到自定义的group中。
optimization: {
  splitChunks: {
     chunks: 'all',
     cacheGroups: {
       commons: {
        minChunks: 2,
        priority: -20,
        name: 'commons',
      }
    }
  }
}
  1. webpack打包资源过大警告
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets: 
  js/commons.532a8.js (515 KiB)
  js/vendor.b2172.js (414 KiB)
  js/BIPage.dafcb.js (477 KiB)
  js/app.c64e5.js (1.2 MiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  app (1.77 MiB)
      js/react.97c29.js
      js/vendor.b2172.js
      js/app.c64e5.js

分析:webpack推荐打包生成的asset单个资源大小和入口资源大小在244 KiB以内。
隐藏:performance: { hints: false }
参考:https://github.com/webpack/webpack/issues/3486

  1. Tree Shaking为何不生效?
    分析:关于lodash,已经通过babel-plugin-lodash实现了Tree Shaking。对于业务模块,由于react-native使用的打包工具metro不支持Tree Shaking,因此不支持。
    解决:如果babel配置文件中使用module:metro-react-native-babel-preset,则可以支持Tree Shaking。但这样会导致react-native项目无法打包。
    参考:https://www.npmjs.com/package/metro-react-native-babel-preset
    https://github.com/facebook/metro/issues/227

2.3 打包优化

2.3.1 代码分割

  1. 同步代码分割
    通过optimization.splitChunks.cacheGroups,对basereactwebreact以及node_modules分别进行同步代码分割,生成appBase.jsreactweb.jsreact.js以及vendors.js
optimization: {
    splitChunks: {
        chunks: 'all',
        maxInitialRequests: 5,
        automaticNameDelimiter: '-',
        cacheGroups: {
            //同步模块代码分割
            appBase: {
                test: /[\\/]public[\\/]base/,
                priority: 0,
                name: 'appBase',
                chunks: 'initial'
            },
            reactWeb: {
                test: /[\\/]lib[\\/]reactweb/,
                priority: 0,
                name: 'reactWeb',
                chunks: 'initial'
            },

            //node_modules代码分割
            react: {
                test: /[\\/]node_modules[\\/](react\.*|redux\.*)/,
                priority: -5,
                name: 'react',
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                name: 'vendor'
            }
        }
    }
}
  1. 异步代码分割
    通过动态import(),对LoginDirectoryReportPageFormPageBIPageWebPageChangePassword进行异步代码分割
Loadable({
    loader: () => import(/*webpackChunkName: "Login"*/'../../platform/view/Login'),
    loading: () => null,
})
  1. 打包
image.png
  1. 动态import()异步代码分割生成的一些bundler体积非常小。可以利用魔法注释中的webpackChunkName合并bundler

2.3.2 Base Tree Shaking

一. 问题分析

  1. base文件夹中绝大部分模块(modules)只在BIPageReportPageFormPage等异步Chunks中使用。为什么没有被打包到这些异步模块中,而是全部被同步代码分割出来了呢?
    即:为什么动态import()异步代码分割对base中模块modules失效?

如果没有对base的同步代码分割,这些base模块会被打包到入口app.js

  1. 分析
    一个模块如果被动态import()对应的异步chunk引入,同时被同步chunk引入,则该module会被打包进入同步chunk对应的bundler

二. 逐个切断依赖关系

  1. 使app chunk依赖树中不引入base/index模块。
    能够最大限度的降低app.jsbundler体积。但app中的依赖树比较复杂,逐个修改的工作量比较大。依赖树中只要有一条线指向base/index模块,就不能解决问题。
  2. 如何查找app chunk依赖树中哪些modules引入了base/index.js
    (1) 注释掉那些动态import()引入的chunks,只打包app对应的chunk
    (2) 通过分析工具可以查看module之间的依赖关系。
    (3) 找到依赖base/index.js的模块,修改依赖关系。
    image.png
image.png

app bundlerplatform bundler过滤base modules。在进入模板页之前,减少js bundler的体积。

三. babel插件

  1. 三个插件

(1) babel-plugin-module-resolver 插件

// Use this:
import MyUtilFn from 'utils/MyUtilFn';
// Instead of that:
import MyUtilFn from '../../../../utils/MyUtilFn';

可以实现webpack中的resolve.alias的功能。
参考:custom-aliases-in-react-native-with-babel
(2) babel-plugin-import

import { TimePicker } from "antd"
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/time-picker');

可以实现webpack中第三方库的Tree Shaking功能。
(3) babel-plugin-transform-imports

//Causes this code:
import { MyModule } from 'my-library';
import { App } from 'my-library/components';
import { Header, Footer } from 'my-library/components/App';
//to become:
import MyModule from 'my-library/MyModule';
import App from 'my-library/components/App';
import Header from 'my-library/components/App/Header';
import Footer from 'my-library/components/App/Footer';

可以实现第三方库与本地代码的Tree Shaking

第一个插件的作用是实现resolve.alias。后两个插件的作用是实现tree shaking。后两个插件和第一个插件共同使用时,后两个插件不生效。

  1. 最佳实践
    由于babel-plugin-module-resolver与另外两个插件共用使用时,另外两个插件不生效。因此,H5端使用webpack自身支持的resolve.alias和另外两个插件的其中之一(推荐babel-plugin-transform-imports)实现Tree ShakingApp端使用babel-plugin-module-resolver 插件实现alias
    image.png
  2. 注意事项

(1) 使用babel-plugin-transform-imports转换插件实现TreeShaking,需要保证一个module对应一个文件。

module.exports = function(importName, matches) {
    if(components.includes(importName)) {
        return `base/components/${importName}`
    }
    if(transMap[importName]) {
        return transMap[importName]
    }
    console.error(importName + ` must define the transform import format!`);
};

(2) 修改transformImport.js中转换函数后需要清除缓存文件

image.png

使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到node_modules目录,将会降级回退到操作系统默认的临时文件目录。

2.3.3 CSS资源

  1. 项目中外部式样式和嵌入式样式比较少,对页面加载影响微乎其微。
  2. 因为与app共用代码。项目中绝大部分样式是通过StyleSheet.create({})以行内形式嵌入,导致打包输出的js文件体积增加。但如果不修改写法,行内样式无法抽出。

2.3.4 图片资源

  1. 图片资源预想的是将小图片转化为base64字符串或者使用CSS spirit。后来发现遇到问题如下:
    image.png

(1) 由于现有icon.js的写法。使用base64字符串会将没有使用到的小图标也打包到js文件中。造成js中图标体积约400KB
(2) CSS spirit图片拼接比较麻烦,现有的以webpack-spritesmithpostcss-sprites为代表的插件也是以css文件为基础,需要修改现有Icon组件的实现方式。即使解决了该问题,雪碧图拼接会将所有小图标拼成一个大图片,包括使用不到的图标。

3. 代码逻辑

  1. 区分不同入口逻辑
    js分包的角度来看,动态import()异步资源加载不适用于用户直接打开模板时的入口,js分包已经做了入口区分。从代码逻辑上来看,不同入口代码逻辑也应该做区分,减少直接打开模板生成的bundler体积。
  2. 异步阻塞逻辑优化
    页面渲染之前的异步回调会推迟页面初始渲染时间。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,142评论 0 3
  • babel 7 的使用的个人理解 最近看了很多关于babel的使用方法,大部分在一些点上都没有说明白,同时给出的代...
    zshawk1982阅读 20,996评论 14 43
  • 1. Node.js 介绍 npm的两层含义 npm 是一个网站,这个网站上托管了几十万个使用 JavaScrip...
    lemonzoey阅读 2,148评论 0 1
  • babel官网 babel 介绍 Babel 是一个通用的多用途 JavaScript 编译器。通过 Babel ...
    锋享前端阅读 1,813评论 0 10
  • 昨天晚上开始就觉得体乏困倦,今天白天上下午又各跳了四十分钟舞,今晚的跑步就变得有点艰难,所以意思一下就回家了...
    风筝2017阅读 171评论 0 0