在SPA应用中,随着项目的推进,功能不断增加,代码量也随之日益增长,导致打包内容也变得更加臃肿,首屏加载慢,部分功能复杂的页面渲染慢,这个时候需要对打包内容进行优化调整。
1. 分析打包内容
打包内容臃肿,想要对症下药,就要先分析下臃肿的原因是什么,umi 内置包模块分析工具 analyze
,通过该工具可以看到打包后各个模块的大小,然后就可以根据实际情况按需进行优化。
通过在package.json
的scripts
配置中配置 ANALYZE=1 umi build
或 ANALYZE=1 umi dev
开启包分析:
"scripts": { "start": "umi dev", "build": "ANALYZE=1 umi build", "analyzeDev": "ANALYZE=1 umi dev", // 开发打包分析"analyzeBuild": "ANALYZE=1 umi build", // 生产打包分析"pack": "umi build && npm run packjs", "packjs": "node scripts/pack.js", "eslint": "eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx ./src", "lint-staged": "lint-staged", "test": "umi-test", "test:coverage": "umi-test --coverage"},
执行npm run build
命令,会使用analyze
默认配置(其他更多配置)自动打开包分析页面,包含打包中所有模块的内容,鼠标悬浮能查看包详细信息,为后续代码拆包进行一个简单的规划,哪些是比较大,需要单独拆出来的。在默认没有代码切割和按需加载配置的情况下,umi最终只会输出一个.js
文件,如下图,是一个高达5.11MB (umi默认会对代码进行压缩)的文件:
2. 产物优化
通过包分析可以看到打包后的JS文件体积为 5.11MB, gizped 后也有 1.51MB。而且最终的打包产物只有一个JS文件和CSS文件,静态资源的分配需要在文件体积和文件数量之间均衡,太大的单文件和数量太多的小文件,都会降低用户体验,所以这里需要将构建产物进行拆包处理,先拆成小的模块。
1. 按需加载-配置dynamicImport
umi自带的配置项,默认关闭,该情况下只输出一个JS和CSS文件,即:umi.js
和 um.css
该配置可以实现按需加载资源:
是否启用按需加载,即是否把构建产物进行拆分,在需要的时候下载额外的 JS 再执行。
当项目越来越庞大时,构建的文件也会越来越大,虽然默认的构建产物简单,部署方便,但是带来的结果就是加载资源慢,导致白屏时间长,用户体验差。下面进行按需加载的拆包配置
// 只在生产环境下进行按需加载dynamicImport: process.env.NODE_ENV==='production'? { loading: '@/Loading'} : undefined,
- 这里的
loading
配置为一个路径,指向一个loading组件文件,自定义构建在/src
目录下即可。
进行按需加载的拆包后,输出的文件如下:
可以发现,这里的umi.js
文件小了很多,而且打包的产物多了很多文件,但是大部分文件中都包含一些重复的node_modules/
目录下的文件,需要对这些文件再次进行拆包抽离的优化。
2. 代码切割,减少包尺寸-配置webpack-splitChunks
splitChunks为webpack自带的配置项,可以提取一些公共依赖,将复用的antd、echarts以及node_moudles目录下的其他文件进行抽离:
chunks: ['echarts', 'vendors', 'antd', 'umi'], chainWebpack(memo) { memo.optimization.splitChunks({ chunks: 'all', //async异步代码分割 initial同步代码分割 all同步异步分割都开启automaticNameDelimiter: '.', name: true, minSize: 30000, // 引入的文件大于30kb才进行分割//maxSize: 50000, // 50kb,尝试将大于50kb的文件拆分成n个50kb的文件minChunks: 1, // 模块至少使用次数// maxAsyncRequests: 5, // 同时加载的模块数量最多是5个,只分割出同时引入的前5个文件// maxInitialRequests: 3, // 首页加载的时候引入的文件最多3个// name: true, // 缓存组里面的filename生效,覆盖默认命名cacheGroups: { echarts: { name: 'echarts', test: /[\\/]node_modules[\\/](echarts)[\\/]/, priority: -9, enforce: true, }, antd: { name: 'antd', test: /[\\/]node_modules[\\/](@ant-design|antd|antd-mobile)[\\/]/, priority: -10, enforce: true, }, vendors: { name: 'vendors', test: /[\\/]node_modules[\\/]/, priority: -11, enforce: true, }, }, }); },
- 需要注意,这里的
chunks
配置表示文件加载顺序,注意文件间的依赖关系加载文件。
公共依赖抽取之后,打包后的文件资源如下,打包后的文件从7.64MB缩小到了5.6MB,同时抽取了公共文件,可以进行缓存处理
3. 大的三方包从 cdn 引入-配置externals
对于一些大尺寸依赖,比如图表库、antd 等,可尝试通过 externals 的配置引入相关 umd 文件,减少编译消耗,同时能减小包文件的体积
该配置可以将一些公共的三方包以CDN的方式引入,不再打包到静态资源文件中,进一步减小构建产物的体积,也在一定程度上缓解服务器压力
// 配置 externalexternals: { 'react': 'window.React', 'react-dom': 'window.ReactDOM', }, // 引入被 external 库的 scripts// 区分 development 和 production,使用不同的产物scripts: process.env.NODE_ENV==='development'? [ 'https://gw.alipayobjects.com/os/lib/react/16.14.0/umd/react.development.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.14.0/umd/react-dom.development.js', ] : [ 'https://gw.alipayobjects.com/os/lib/react/16.14.0/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.14.0/umd/react-dom.production.min.js', ],
配置完成后, .html
文件中引入资源的方式就会编程cdn外链的方式来引入:
- 该方案不适合内网项目,无法访问外部资源的部署环境
至此,大部分的优化都已经结束了,vendor.js中的文件可以进行进行抽离优化,因为浏览器同域名下请求资源的数量是有上限的,所以在减小静态资源文件的大小的同时,控制好静态资源的数量。
3. 实现预加载
前文说到了按需加载的拆包方案,使静态资源需要的时候再进行加载,但是如果某个页面过于复杂,打包后该页面的JS文件也会比较大,需要点击该页面的时候才能加载,这同样会带来一个问题,虽然有loading,但是体验也并不太好,是如果在进入系统后在浏览器空闲时间能够预加载一些比较庞大的页面,那就达到需求了。
1. 按需加载组件dynamic
dynamic
是umi自带的API,用于实现一个异步加载的组件:
importReactfrom'react'; import { Spin } from'antd'; import { dynamic } from'umi'; exportdefaultdynamic({ loader: asyncfunction () { const { default: AddEdit } =awaitimport(/* webpackChunkName: "examineAddEdit" */'./AddEdit'); returnAddEdit; }, loading: () => ( <divstyle={{ width: '100vh', height: '100vh', paddingTop: '20vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}><Spintip="页面加载中..."/></div> ) });
这里的关键是动态的import()
,借用网上的一段译文来了解下import()
函数:
ES2015 Loader 规范 定义了 import() 方法,可以异步的、动态的加载模块,与所加载的模块没有静态连接关系,这点也是与import语句不相同之一。import函数的返回值是promise对象,可以使用.then
和.catch
方法进行接收数据处理,import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口,其允许模块路径动态生成。import函数可以放在任何地方,因为它是运行时执行的,什么时候执行到它,就什么时候进行指定模块的加载,所以它可以在条件语句和函数中进行动态的加载。
该异步组件可以像其他普通组件一样被调用即可,它会在需要的时候进行异步加载。
2. 魔法注释-Magic Comments
Magic Comments
是一种内联注释,通过在 import 中添加注释,可以进行诸如给chunk
命名或选择不同模式的操作。
上例中的异步组件即使用了魔法注释的一个webpackChunkName
:
/* webpackChunkName: "examineAddEdit" */
新 chunk 的名称。 添加此注释后,将单独的给 chunk 命名为 [my-chunk-name].js 而不是 [id].js
除了该属性外,还有其他四个属性分别为:
webpackPrefetch
:告诉浏览器将来可能需要该资源来进行某些导航跳转。查看指南,了解有关更多信息 how webpackPrefetch works。webpackPreload
:告诉浏览器在当前导航期间可能需要该资源。 查阅指南,了解有关的更多信息 how webpackPreload works。webpackMode
:从 webpack 2.6.0 开始,可以指定以不同的模式解析动态导入.webpackInclude
:在导入解析(import resolution)过程中,用于匹配的正则表达式。只有匹配到的模块才会被打包。webpackExclude
:在导入解析(import resolution)过程中,用于匹配的正则表达式。所有匹配到的模块都不会被打包。
webpackPrefetch
是这里要使用的关键属性,前面说到按需加载的资源,会在进入该路由后再进行静态资源的加载,所以在index.html
中看到的大部分都是这样的:
进入页面后再进行静态资源的请求
使用预加载的魔法注释,来实现预加载:
importReactfrom'react'; import { Spin } from'antd'; import { dynamic } from'umi'; exportdefaultdynamic({ loader: asyncfunction () { const { default: AddEdit } =awaitimport(/* webpackChunkName: "examineAddEdit" *//* webpackPrefetch: true */'./AddEdit'); returnAddEdit; }, loading: () => ( <divstyle={{ width: '100vh', height: '100vh', paddingTop: '20vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}><Spintip="页面加载中..."/></div> ) });
这种预加载的组件在进入系统的时候就会以<link rel="prefetch" as="script">
的形式预拉取代码:
可以看到在进入该页面后请求资源是没有消耗时间的,这尤其适用于非首页的某个路由下的比较复杂的页面,进行资源的预加载,跳转至该页面下会快速呈现出页面。