1. 9.0版本
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)
(1) 动态路由生成js
文件没有自定义名
(2) 0.js
和1.js
代码几乎都是重复的。
(3) 2.js
、3.js
、4.js
动态路由生成的代码体积太小。
- 设置打包配置
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/'
},
- 自定义
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'
- 打包分析
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)
1.2 图表资源提前加载
- 图表资源加载分析
在ChartView
组件的didMount
中请求加载图表资源,导致图表空白1-2s
才能渲染出来。 - 提前加载图表资源
<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.3 打包区分不同入口
- 多入口分析
项目支持多入口,不同入口打开同一个html
页面。页面初始加载app.js
、vendor.js
、manifest.js
资源。当路由跳转到platform
或FRPage
对应页面时,动态加载platform.js
和FRPage.js
。
当从模板页(cpt
、frm
)进入项目时,FRPage.js
仍通过动态路由的方式加载,加载时机有些延迟,增加了页面打开时间(客户反映提升不大)。 - 打包生成多个
html
页面,不同入口打开不同的html
页面。当从登录页进入项目时,使用动态路由;当直接打开cpt
或者frm
模板时,不使用动态路由,提前加载对应js
资源。 - 打包分析
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)
1.4 代码分割
-
Performance
分析图
- 生成的
app.js
体积很大,但CommonsChunkPlugin
只适用于多入口文件公共模块代码分割。本项目入口只有一个index.web.js
,不符合CommonsChunkPlugin
插件的应用场景。 -
webpack
升级
(1)Cyclic dependency error
解决:https://github.com/marcelklehr/toposort/issues/20
(2) 其他见10.0 webpack
升级遇到问题及解决方案。 - 代码分割
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'
}
}
}
}
-
时间对比
2. 10.0版本打包优化
2.1 知识扩展
2.2 问题分析
-
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
。
解决:升级happypack
为 5.X
版本。
-
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
-
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 20
、npm dedupe
、删除node_modules
、删除package.lock.json
、npm install
。
方法二:npm install webpack@4.28.4
参考:https://github.com/webpack/webpack/issues/8656
-
uglifyjs-webpack-plugin
压缩动态import()
语法报错
分析:uglifyjs-webpack-plugin
只支持ES5
代码压缩,不支持ES6
代码压缩。
解决:使用terser-webpack-plugin
替换掉uglifyjs-webpack-plugin
。
参考:https://webpack.docschina.org/plugins/terser-webpack-plugin -
import()
动态加载魔法注释webpackChunkName
失效
分析:①在项目中使用了module:metro-react-native-babel-preset
,该preset
是babel
为ReactNative
应用提供的,ReactNative
应用默认使用它转化代码。②该preset
支持动态import()
,无需再使用@babel/plugin-syntax-dynamic-import
。③ 该preset
会在babel
打包过程中删除注释内容。
解决: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 - 打包生成的
bundle
中含有动态import()
之外的js
文件
分析:optimization.splitChunks
在进行代码分割时,会默认将不同chunk
引入的相同modules
进行分割,避免这些代码重复打包到不同的bundle
。
解决:配置optimization.splitChunks
中的cacheGroups
,将共用代码提取到自定义的group
中。
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
commons: {
minChunks: 2,
priority: -20,
name: 'commons',
}
}
}
}
-
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
-
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 代码分割
- 同步代码分割
通过optimization.splitChunks.cacheGroups
,对base
、reactweb
、react
以及node_modules
分别进行同步代码分割,生成appBase.js
、reactweb.js
、react.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'
}
}
}
}
- 异步代码分割
通过动态import()
,对Login
、Directory
、ReportPage
、FormPage
、BIPage
、WebPage
、ChangePassword
进行异步代码分割。
Loadable({
loader: () => import(/*webpackChunkName: "Login"*/'../../platform/view/Login'),
loading: () => null,
})
- 打包
- 动态
import()
异步代码分割生成的一些bundler
体积非常小。可以利用魔法注释中的webpackChunkName
合并bundler
2.3.2 Base Tree Shaking
一. 问题分析
-
base
文件夹中绝大部分模块(modules
)只在BIPage
、ReportPage
、FormPage
等异步Chunks
中使用。为什么没有被打包到这些异步模块中,而是全部被同步代码分割出来了呢?
即:为什么动态import()
异步代码分割对base
中模块modules
失效?
如果没有对
base
的同步代码分割,这些base
模块会被打包到入口app.js
。
- 分析
一个模块如果被动态import()
对应的异步chunk
引入,同时被同步chunk
引入,则该module
会被打包进入同步chunk
对应的bundler
。
二. 逐个切断依赖关系
- 使
app chunk
依赖树中不引入base/index
模块。
能够最大限度的降低app.js
的bundler
体积。但app
中的依赖树比较复杂,逐个修改的工作量比较大。依赖树中只要有一条线指向base/index
模块,就不能解决问题。 - 如何查找
app chunk
依赖树中哪些modules
引入了base/index.js
?
(1) 注释掉那些动态import()
引入的chunks
,只打包app
对应的chunk
。
(2) 通过分析工具可以查看module
之间的依赖关系。
(3) 找到依赖base/index.js
的模块,修改依赖关系。
对
app bundler
和platform bundler
过滤base modules
。在进入模板页之前,减少js bundler
的体积。
三. babel插件
- 三个插件
(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
。后两个插件和第一个插件共同使用时,后两个插件不生效。
- 最佳实践
由于babel-plugin-module-resolver
与另外两个插件共用使用时,另外两个插件不生效。因此,H5
端使用webpack
自身支持的resolve.alias
和另外两个插件的其中之一(推荐babel-plugin-transform-imports
)实现Tree Shaking
。App
端使用babel-plugin-module-resolver
插件实现alias
。
- 注意事项
(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
中转换函数后需要清除缓存文件
使用默认的缓存目录
node_modules/.cache/babel-loader
,如果在任何根目录下都没有找到node_modules
目录,将会降级回退到操作系统默认的临时文件目录。
2.3.3 CSS资源
- 项目中外部式样式和嵌入式样式比较少,对页面加载影响微乎其微。
- 因为与
app
共用代码。项目中绝大部分样式是通过StyleSheet.create({})
以行内形式嵌入,导致打包输出的js
文件体积增加。但如果不修改写法,行内样式无法抽出。
2.3.4 图片资源
- 图片资源预想的是将小图片转化为
base64
字符串或者使用CSS spirit
。后来发现遇到问题如下:
(1) 由于现有icon.js
的写法。使用base64
字符串会将没有使用到的小图标也打包到js
文件中。造成js
中图标体积约400KB
。
(2) CSS spirit
图片拼接比较麻烦,现有的以webpack-spritesmith
、postcss-sprites
为代表的插件也是以css
文件为基础,需要修改现有Icon
组件的实现方式。即使解决了该问题,雪碧图拼接会将所有小图标拼成一个大图片,包括使用不到的图标。
3. 代码逻辑
- 区分不同入口逻辑
从js
分包的角度来看,动态import()
异步资源加载不适用于用户直接打开模板时的入口,js
分包已经做了入口区分。从代码逻辑上来看,不同入口代码逻辑也应该做区分,减少直接打开模板生成的bundler
体积。 - 异步阻塞逻辑优化
页面渲染之前的异步回调会推迟页面初始渲染时间。