WdBly Blog

懂事、有趣、保持理智

WdBly Blog

懂事、有趣、保持理智

周维 | Jim

603927378@qq.com

webpack性能优化(上)

webpack常用的优化手段总结

webpack做为一款当下最流行的前端构建工具,一直以来以门槛太高而受人诟病,且没有一个通用的配置适合所有的项目,为此我们不得不开心的(大误😃)手动配置我们的项目,开启我们的配置工程师之路,本文主要是对webpack的一些基础配置的解释和我们能做的一些优化工作,适合有使用过webpack的前端开发者阅读,各位在阅读过程中不妨打开自己的项目来跟着我一起优化。

分割配置文件 dev prod

通常我们的项目会有开发环境和生产环境,而开发环境我们配置的目标是构建更快,模块热替换,能从chrome控制台报错信息对应的源码的错误处(source map)等。生产环境我们更加关注chunk分离,缓存,安全,tree shaking等优化点。
当然对于开发和生产环境的配置文件肯定是不完全相同的,所以我们将不同环境的配置文件分离开,同时将共用部分抽出来,最后使用webpack-merge插件整合。

//参考使用方法 const webpackMerge = require('webpack-merge'); const additionalConfigPath = { development: './webpack.dev.config.js', production: './webpack.prod.config.js' }; const baseConfig = {}; module.exports = webpackMerge( baseConfig, require(additionalConfigPath[process.env.NODE_ENV]) );

在下方的优化点中 我会标注这些点适用的环境 dev=开发环境 prod=生产环境

代码分离 prod

代码分离能够将工程代码分离到各个文件中,然后按需加载或并行加载这些文件,也用于获取更小的 bundle,以及控制资源加载优先级

webpack有三种常用的代码分离方法:

  • 入口起点:配置多入口,输出多个chunk。
  • 防止重复:使用 CommonsChunkPlugin 去重和分离 chunk(webpack4虽然废弃,但有代替方法)
    -动态导入:常用于按需加载的模块分离

入口起点

//多入口配置 最终输出两个chunk module.exports = { entry: { index: 'index.js', module: 'module.js' }, output: { //对于多入口配置需要指定[name]否则会出现重名问题 filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };

问题所在:入口chunks中如果包含重复的模块,如jquery,那些重复模块都会被引入到各个bundle中

去除重复

对于webpack4 < 4 我们使用CommonsChunkPlugin插件

entry: { "app": "entry-client.js" } //首先将app入口的公共模块提取出来 new webpack.optimize.CommonsChunkPlugin({ name: 'common', filename: '[name].[chunkHash:6].common.js', chunks: ['app'] }) //将 vendor runtime分离出来 new webpack.optimize.CommonsChunkPlugin({ name: ['vendor','runtime'], filename: '[name].[chunkHash:6].bundle.js', //这个配置保证没其它的模块会打包进 vendor chunk minChunks: Infinity }),

chunks 选项代表了从哪个入口分离公共文件,这里我们使用我们项目的主入口 app来分离自定义的公共模块

首先将自定义的公共模块单独抽离出来,这样做的目的是方便我们做缓存,当自定义模块更新时不会影响到vendor文件的hash值。

然后将第三方库文件 和 webpack运行文件分离。

这样我们的vendor就是非常干净的了,只包含第三方库,可以做长效缓存。这个地方需要分离webpack运行文件runtime,因为无论我们是否修改了项目文件,每次build项目时的runtime文件的hash值总是会发生变化的,需要单独打出来。

对于webpack4 我们需要实现同样的目标

// 提前自定义公共模块 optimization: { splitChunks: { chunks: "all", minSize: 20000, //其他入口chunk引用的次数 minChunks: 1, //默认使用name + hash生成文件名 name: true, //使用自定义缓存组 cacheGroups: { //公共模块 commons: { name: 'common', //缓存优先级设置 priority: 10, //从入口chunk提取 chunks: 'initial' }, //提取第三方库 vendors: { //符合条件的放入当前缓存组 test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all" }, } } } //提取webpack运行文件 runtimeChunk: { name: 'manifest' },

上面的配置大概意思是对所有满足条件的chunk开启代码拆分。

优先级(priority):多个分组冲突时决定把代码放在哪块

通过splitChunks 和 runtimeChunk插件我们能达到同样的拆分和缓存目的

路由组件动态导入

webpack可以将异步加载的组件打包为chunk,同时按需加载此chunk。最为常见是我们的路由组件,我们可以将路由入口组件动态导入以达到异步加载的目的。

异步组件的导入主要有两种方式
1:ES6 stage3的提案 import()
2:webpack 特定的 require.ensure

//import 方式 const Login = () => { import(/* webpackChunkName: 'Login' */'./Login') } //require.ensure 方式 require.ensure([], function(require){ require('./Login'); },"Login"); //异步组件打包的chunk命名 使用webpackChunkName 或者 //使用require.ensure的第三个参数 //对于webpack2/3需要配置chunkFilename来接收配置的name output: { //非入口 chunk 的名称 chunkFilename: '[name].[chunkHash:6].chunk.js' } //webpack4配置name为true(默认值) optimization: { splitChunks: { //chunk name由块名和hash值自动生成 name: true } }

require.ensure 现在依赖于原生的 Promise。如果在不支持 Promise 的环境里使用 require.ensure,你需要添加 polyfill。

普通组件的异步加载

既然我们可以使用import()和require.ensure来动态导入组件那其实在项目中我们更是做到可以基于组件的异步加载。

比如我们的首页是由20个组件组成,但是首屏只会显示15个组件,另外的5个组件可能是一些弹窗,或底部才显示的这种组件,那么我们完全可以将之使用动态导入抽离出来,在需要的时候加载它,如:

//在异步事件中去导入组件 <div onClick={this.showPopUps}> show-popUps </div> showPopUps = () => { this.popUps = ()=> { import(/*webpackChunkName: 'PopUps'*/'./popUps') } }

这样处理的问题在于,我们在点击按钮弹出浮层时会有一定的延时,因为在点击时我们才从服务端去获取组件,用户体验可能不太优秀。

这时我们可以使用一种预测加载的技术prefetch。prefetch提示浏览器这个资源将来可能需要,浏览器会选择空闲的时间去下载此资源。prefetch常常用于加速下一次操作。

<link rel="prefetch">

兼容性问题:从上述可以得知,prefetch时基于浏览器实现的,存在一定的兼容性问题,在safari上还没由得到支持。

在不支持的浏览器中并不会影响我们页面的功能

使用:preload-webpack-plugin
使用条件:webpack > 2.2 且需要使用 html-webpack-plugin

plugins: [ new HtmlWebpackPlugin(), new PreloadWebpackPlugin({ rel: "prefetch", as: 'script', //包含了哪些chunk,默认值为"asyncChunks" include: 'asyncChunks' }) ] //最终效果类似这样 <link rel="prefetch" as="script" href="chunk.d15e.js">

对于.css结尾的文件 as=style,.woff2结尾的文件as=font,否则as=script,当然你可以像上方那样给一个确定的值。

对于include的值,其实我们并不是需要所有的异步chunk都使用prefetch,所以我们需要重设include的值,像这样:

//1 给定需要prefetch的块的名字(chunkName) include: ["PopUps","Login"] //2 或者在动态导入时指定Prefetch this.popUps = () => { import(/* webpackChunkName: 'PopUps', webpackPrefetch: true */'./PopUps') }

Loader dev prod

为Loader指定作用范围可以加快我们的构建速度,提升开发体验。

{ test: /\.jsx?$/, use: [ "babel-loader" ], exclude: /node_modules/ } //or { test: /\.jsx?$/, use: [ "babel-loader" ], include: /src/ }

webpack解析(resolve) dev

resolve的部分配置过多项,会导致webpack搜索范围变大,效率下降,如没有特殊需求,可以保持默认配置保持开发体验。

//解析模块时应该搜索的目录 modules: ["node_modules"] //default //自动解析确定的扩展,不宜过多 extensions: [".js", ".json"] //default //解析目录时要使用的文件名,不宜过多 mainFiles: ["index"] //default

webpack 外部扩展(Externals) dev prod

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法,什么意思呢?顾名思义,externals可以排除掉我们打包结果中某些依赖:

//排除react 我们打包后的bundle中没有react了 externals: { //将react指向一个全局变量 react react: "react" } //这里的 "react" 会直接去全局变量中寻找 import react from "react"

这种设置通常用于library开发人员,你的library依赖了react,同时使用了你的库的开发人员也依赖了react,最终会造依赖你的库的用户bundle中react被重复打包。

你应该做的是在你的library中将外部依赖放到externals中排除掉,然后将你的依赖暴露给使用你的库的用户,最后你的library应该从外部获取这些扩展依赖,而不是一股脑打包进你自己的bundle中。当然这个依赖在用户的环境中必须存在且可用。

问题所在:对于从一个依赖目录中,调用的多个文件,使用externals 无法一次排除;

import one from 'react/one'; import two from 'react/two'; //上面这种情况只能一个个匹配,或者这通过正则匹配 externals: [ 'react/one', 'react/two', /^react\/.+$/ ]

这里不得不提的是npm依赖管理中的peerDependencies,它的作用正是将我们library中依赖的库暴露给用户的,若是用户没有安装我们库中的必需依赖,npm会抛出错误。

当然如果你不是一个library开发人员,你同样可以使用externals来排除项目中的外部依赖,然后通过其他方式引入成为一个全局变量的方式来优化打包速度。

Dlls 优化构建速度 dev

webpack在每次build时都会将整个项目重新构建一遍,不管这个文件是否发生了改变(当然若文件未更改hash值不变),但是其实我们所依赖的三方库更改并不频繁,若是将三方库抽离出来单独构建,将构建好的目标和构建生成的json对照文件引如我们的项目,这样不用每次都去打包这些代码。

使用 DllPlugin 将更改不频繁的代码进行单独编译。这将改善引用程序的编译速度,即使它增加了构建过程的复杂性。

//新建 webpack.dll.conf.js module.exports = { entry: { vendor: [react, ...] }, output: { filename: 'dll.[name].js' }, plugins: [ new webpack.DllPlugin({ path: '[name]-manifest.json' }) ] } //webpack.base.conf.js const manifest = require('./vendor-manifest.json') plugins: [ new webpack.DllReferencePlugin({ manifest }) ]

最后需要手动将生成的dll.[name].js引入到html文件中去,当然这一步骤可以用assets-webpack-plugin插件优化。

都阅读到这里的看官不妨点个赞或移步github点个星星再走如何。