Open happylindz opened 6 years ago
原文地址
最近在团队分享一些 webpack 技巧,于是便准备梳理出一篇博文总结一下,不过由于讲的内容太多了,无法描述的很详细,更多地是提供一个思路,希望读者感兴趣可以动手去实践实践。
配置多份 webpack 配置,通过 webpack-merge 进行合并,
├── common.js ├── dll.config.js ├── webpack.base.config.js ├── webpack.dev.config.js ├── webpack.prod.config.js // etc 同构配置,node middleware 等等
然后通过 npm scripts 执行 webpack 命令:
{ "scripts": { "dev": "webpack-dev-server --config ./webpack.dev.config.js", "build": "webpack --config ./webpack.prod.config.js", "start": "npm run dev", "pre": "webpack --config ./dll.config.js" }, }
node_modules/.bin/webpack-dev-server
node_modules/.bin/
想开启热更新,首先需要在入口文件进行配置:
// 入口文件 if(module.hot) { module.hot.accept(['./App'], () => { render(<App />, document.getElementById('app')) }) }
模块热更新机制:
最简单的方式:
直接在命令里面加上 webpack-dev-server --hot 即可开启热更新。
webpack-dev-server --hot
该参数相当于是做了:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin'); module.exports = { plugins: [ new HotModuleReplacementPlugin(), ], devServer:{ hot: true, } };
当然如果你想要更加定制化的控制,你需要在 webpack 配置进行额外配置:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin'); module.exports = webpackMerge(baseConfig, { plugins: [ new HotModuleReplacementPlugin(), ], devServer: { // 每次构建时候自动打开浏览器并访问网址 open: true, // 开启热更新 hot: true, // 设置静态资源地址如:/public,从这获取你想要的一些外链资源,图片。 contentBase: DIST_PATH, // 设置端口号 port: 9000, // 将热更新代码注入到模块中 inline: true, // 如果你希望服务器外部可访问 host: "0.0.0.0", // 设置 proxy 代理 proxy: { context: ['/api'], target: "//www.proxy.com", pathRewrite: {"^/api" : ""} }, // 设置 https https: true } });
关于 webpack 热更新原理我就不说了,感兴趣可以看下面两篇文章:
module.exports = { devtool: 'source-map', }
方便调试源代码
在大型应用减少每次构建的时间十分重要,动不动几十秒的编译时间令人发指,我在经过一些实践,总结下面一些方式,至少可以让你的编译速度快 1-2 倍。
首先第一点:缩小 Babel 的编译范围,并使用 webpack cache 缓存模块。
module.exports = { // 减小模块的查找范围 resolve: { modules: [path.resolve(__dirname, 'node_modules')], }, module: { rules: [ { test: /\.js?$/, use: [{ loader: 'babel-loader', query: { // 将 babel 编译过的模块缓存在 webpack_cache 目录下,下次优先复用 cacheDirectory: './webpack_cache/', }, }], // 减少 babel 编译范围,忘记设置会让 webpack 编译慢上好几倍 include: path.resolve(__dirname, 'src'), }, ] }, }
通过这步可以快上好几秒,另外你可以使用 DLLPlugin 预先打包好第三方库,避免每次都要去编译。开启 DLLPlugin 需要你额外配置一份 webpack 配置。
// dll.config.js const webpack = require('webpack'); const path = require('path'); const DllPlugin = require('webpack/lib/DllPlugin') const vendors = [ 'react', 'react-dom', 'react-router', 'redux', 'react-redux', 'jquery', 'antd', 'lodash', ] module.exports = { entry: { 'dll': vendors, }, output: { filename: '[name].js', path: path.resolve(__dirname, 'public'), library: '__[name]__lib', }, plugins: [ new DllPlugin({ name: '__[name]__lib', path: path.join(__dirname, 'build', '[name].manifest.json'), }), ] }
运行则会在 public 目录下得到 dll.js 和 dll.manifest.json 文件,然后需要在开发配置文件中关联。
dll.js
dll.manifest.json
const webpack = require('webpack'); module.exports = webpackMerge(baseConfig, { plugins: [ new DllReferencePlugin({ manifest: require('./public/dll.manifest.json'), }), ] });
另外需要在你的 html 模板里面引入 dll.js,webpack 不会自动帮你引入,用好这一步编译速度应该能快一倍左右的时间。
第三点就是使用 happypack 开启多核构建,webpack 之所以慢,是因为由于有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,webpack 构建慢的问题会显得严重。 也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。
在整个 webpack 构建流程中,最耗时的流程可能就是 loader 对文件的转换操作了,因为要转换的文件数据巨多,而且这些转换操作都只能一个个挨着处理。 Happypack 的核心原理就是把这部分任务分解到多个进程去并行处理,从而减少了总的构建时间。
需要配置哪些 loader 使用 Happypack 就要改写那些配置,比如你想要修改 babel 为多核编译:
module.exports = { module: { rules: [ { test: /\.js?$/, use: ['happypack/loader?id=babel'], include: path.resolve(__dirname, 'src'), }, ] }, plugins: [ new HappyPack({ id: 'babel', loaders: [{ loader: 'babel-loader', query: { cacheDirectory: './webpack_cache/', }, }], }) ], };
设置 id=babel,webpack 会去找 plugins 中的 id 为 babel 的插件进行处理。配置其它的 loader 的方式也是类似,不过需要注意的是有的 loader 不支持多核编译。通过这一步应该至少能让你的编译速度快 1/3。
id=babel
最后一点是不要使用 webpack 里 css 模块化方案,我这里指的模块化指的是 css-loader 提供的模块化方式,我们先来看下它是怎么做的,首先它需要在你的 loader 中进行额外配置。
module.exports = webpackMerge(baseConfig, { module: { rules: [ { test: /\.css/, use: [ 'style-loader', { loader: 'css-loader', options: { // 开启 css 模块 modules: true, // 设置命名格式 localIdentName: '[name]__[hash:base64:5]' } } ] }, ] }, }
如果通过这种 css 模块化的方式,意味着你在写 React 组件的时候,需要这样去设置:
import styles from './index.css'; class Index extends React.Component { render() { return ( <div className={ styles.recursive }> xxxx <h1 className={ styles.header }></h1> </div> ); } } export default Index;
它相当于是在输出 css 文件的时候做了一层原名称到新名称的一次转化来保证 css 模块化的特性,输出的值就像这样:
Object { recursive: 'recursive__abc53xxxx', xxxxx: 'xxxxx__def884xxx', }
这样做不好的点在哪:
所以如果想要使用 css 模块化的可以尽量选择其它方案,比如 styledComponents 或者自己添加命名空间等等。
在发布上线的时候就需要考虑到很多性能优化的因素,比如如何有效地去利用浏览器的缓存,如何减少打包文件的体积等等这些因素都值得去优化。
关于如何高效地利用浏览器缓存,之前写过一篇文章详细描述了 webpack 持久化缓存实践,感兴趣可以看看。
我这里做个总结,我认为 webpack 在浏览器缓存需要做到以下几点:
那么我们要怎样通过 webpack 来完成上面的步骤呢?
首先不建议线上发布直接全部使用 DLLPlugin 插件来开启浏览器缓存,DLLPlugin 本身有几个缺点:
我认为的正确的姿势是:
具体如何拆分模块,我在 webpack 持久化缓存实践 已经说明,这里不再赘述。
想要减少打包后的体积,就需要使用到 webpack2 提供的 tree shaking 功能和 webpack3 提供的 scope hoisting 功能。
想要 tree shaking 生效,下面四点值得注意:
首先,模块引入要基于 ES6 模块机制,不再使用 commonjs 规范,因为 es6 模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后清除没用的代码。而 commonjs 的依赖关系是要到运行时候才能确定下来的。
另外对于引用第三方模块使用 tree shaking 功能,可以设置 mainFields 用于配置采用哪个字段作为模块的入口描述。 为了让 tree shaking 对 es6 生效,需要配置 webpack 的文件寻找规则为如下:
mainFields
module.exports = { resolve: { // 针对 npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件 mainFields: ['jsnext:main', 'browser', 'main'] }, };
对于一些死代码,就像下面这样:其大致原理是借助环境变量去判断执行哪个分支。
if (process.env.NODE_ENV === 'production') { console.log('你正在线上环境'); } else { console.log('你正在使用开发环境'); }
通过 shell 脚本的方式去定义的环境变量,例如 NODE_ENV=production webpack,webpack 是不认识的,对 webpack 需要处理的代码中的环境区分语句是没有作用的。
在构建线上环境代码时,需要给当前运行环境设置环境变量 NODE_ENV = 'production',webpack 相关配置如下:
const DefinePlugin = require('webpack/lib/DefinePlugin'); module.exports = { plugins: [ new DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }), ], };
这样设置后 tree shaking 能有效清除跟生产环境无关的代码。
最后需要强调,webpack 只是指出了哪些函数用上了哪些没用上,要剔除用不上的代码还得经过 UglifyJS 去处理一遍。 需要开启代码压缩, tree shaking 才能真正将无用的代码消除。
如果想要开启 Scope hoisting,需要在额外配置 ModuleConcatenationPlugin 插件,并且 Scope hoisting 对下面的情况不生效:
这些我在 深入理解 webpack 文件打包机制 都有详细阐述,这里不多说了。
压缩代码可以使用 UglifyJsPlugin 这个插件对代码进行压缩,不过用过 UglifyJsPlugin 的你一定会发现在构建用于开发环境的代码时很快就能完成,但在构建用于线上的代码时构建一直卡在一个时间点迟迟没有反应,其实卡住的这个时候就是在进行代码压缩。
由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多。
遇到这种情况可以改用 ParallelUglifyPlugin 插件,当 webpack 有多个 JS 文件需要输出和压缩时,原本会使用 UglifyJsPlugin 去一个个挨着压缩再输出, 但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJsPlugin 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。
压缩 CSS 代码的话,因为使用到 extract-text-webpack-plugin 插件将代码从 js 中分离出来,可以通过 optimize-css-assets-webpack-plugin 插件进行压缩,详细的配置项可以看:optimize-css-assets-webpack-plugin
上面讲了很多优化方式,但是无法面面俱到,对此你需要对打包输出结果做分析,以确认下一步的优化思路。这里推荐两个打包分析工具:
简单地介绍下第二种方式,接入方式很简单:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin() ] }
当你启动 webpack 时候,会唤起浏览器弹出 treemap,通过分析图可以清楚地看到:
本文更多地介绍一些思路,详细的优化步骤可以自己去尝试,你肯定会有更多的收获的!
参考链接:
大神,写的太好了,手动点赞
原文地址
webpack 大型应用优化实践
前言
最近在团队分享一些 webpack 技巧,于是便准备梳理出一篇博文总结一下,不过由于讲的内容太多了,无法描述的很详细,更多地是提供一个思路,希望读者感兴趣可以动手去实践实践。
我认为理想的 webpack 配置:
配置多份 webpack 配置,通过 webpack-merge 进行合并,
然后通过 npm scripts 执行 webpack 命令:
node_modules/.bin/webpack-dev-server
,npm scripts 会自动把node_modules/.bin/
下的指令添加到环境中。区分好开发环境和生产环境:
开发过程:
优化开发体验
自动刷新 -> 模块热更新:
想开启热更新,首先需要在入口文件进行配置:
模块热更新机制:
最简单的方式:
直接在命令里面加上
webpack-dev-server --hot
即可开启热更新。该参数相当于是做了:
当然如果你想要更加定制化的控制,你需要在 webpack 配置进行额外配置:
关于 webpack 热更新原理我就不说了,感兴趣可以看下面两篇文章:
配置 sourcemap
方便调试源代码
减少构建时间
在大型应用减少每次构建的时间十分重要,动不动几十秒的编译时间令人发指,我在经过一些实践,总结下面一些方式,至少可以让你的编译速度快 1-2 倍。
首先第一点:缩小 Babel 的编译范围,并使用 webpack cache 缓存模块。
通过这步可以快上好几秒,另外你可以使用 DLLPlugin 预先打包好第三方库,避免每次都要去编译。开启 DLLPlugin 需要你额外配置一份 webpack 配置。
运行则会在 public 目录下得到
dll.js
和dll.manifest.json
文件,然后需要在开发配置文件中关联。另外需要在你的 html 模板里面引入 dll.js,webpack 不会自动帮你引入,用好这一步编译速度应该能快一倍左右的时间。
第三点就是使用 happypack 开启多核构建,webpack 之所以慢,是因为由于有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,webpack 构建慢的问题会显得严重。 也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。
在整个 webpack 构建流程中,最耗时的流程可能就是 loader 对文件的转换操作了,因为要转换的文件数据巨多,而且这些转换操作都只能一个个挨着处理。 Happypack 的核心原理就是把这部分任务分解到多个进程去并行处理,从而减少了总的构建时间。
需要配置哪些 loader 使用 Happypack 就要改写那些配置,比如你想要修改 babel 为多核编译:
设置
id=babel
,webpack 会去找 plugins 中的 id 为 babel 的插件进行处理。配置其它的 loader 的方式也是类似,不过需要注意的是有的 loader 不支持多核编译。通过这一步应该至少能让你的编译速度快 1/3。最后一点是不要使用 webpack 里 css 模块化方案,我这里指的模块化指的是 css-loader 提供的模块化方式,我们先来看下它是怎么做的,首先它需要在你的 loader 中进行额外配置。
如果通过这种 css 模块化的方式,意味着你在写 React 组件的时候,需要这样去设置:
它相当于是在输出 css 文件的时候做了一层原名称到新名称的一次转化来保证 css 模块化的特性,输出的值就像这样:
这样做不好的点在哪:
所以如果想要使用 css 模块化的可以尽量选择其它方案,比如 styledComponents 或者自己添加命名空间等等。
发布上线:
在发布上线的时候就需要考虑到很多性能优化的因素,比如如何有效地去利用浏览器的缓存,如何减少打包文件的体积等等这些因素都值得去优化。
高效利用浏览器缓存
关于如何高效地利用浏览器缓存,之前写过一篇文章详细描述了 webpack 持久化缓存实践,感兴趣可以看看。
我这里做个总结,我认为 webpack 在浏览器缓存需要做到以下几点:
那么我们要怎样通过 webpack 来完成上面的步骤呢?
首先不建议线上发布直接全部使用 DLLPlugin 插件来开启浏览器缓存,DLLPlugin 本身有几个缺点:
我认为的正确的姿势是:
具体如何拆分模块,我在 webpack 持久化缓存实践 已经说明,这里不再赘述。
减少打包文件的体积
想要减少打包后的体积,就需要使用到 webpack2 提供的 tree shaking 功能和 webpack3 提供的 scope hoisting 功能。
想要 tree shaking 生效,下面四点值得注意:
首先,模块引入要基于 ES6 模块机制,不再使用 commonjs 规范,因为 es6 模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后清除没用的代码。而 commonjs 的依赖关系是要到运行时候才能确定下来的。
另外对于引用第三方模块使用 tree shaking 功能,可以设置
mainFields
用于配置采用哪个字段作为模块的入口描述。 为了让 tree shaking 对 es6 生效,需要配置 webpack 的文件寻找规则为如下:对于一些死代码,就像下面这样:其大致原理是借助环境变量去判断执行哪个分支。
通过 shell 脚本的方式去定义的环境变量,例如 NODE_ENV=production webpack,webpack 是不认识的,对 webpack 需要处理的代码中的环境区分语句是没有作用的。
在构建线上环境代码时,需要给当前运行环境设置环境变量 NODE_ENV = 'production',webpack 相关配置如下:
这样设置后 tree shaking 能有效清除跟生产环境无关的代码。
最后需要强调,webpack 只是指出了哪些函数用上了哪些没用上,要剔除用不上的代码还得经过 UglifyJS 去处理一遍。 需要开启代码压缩, tree shaking 才能真正将无用的代码消除。
如果想要开启 Scope hoisting,需要在额外配置 ModuleConcatenationPlugin 插件,并且 Scope hoisting 对下面的情况不生效:
这些我在 深入理解 webpack 文件打包机制 都有详细阐述,这里不多说了。
压缩代码
压缩代码可以使用 UglifyJsPlugin 这个插件对代码进行压缩,不过用过 UglifyJsPlugin 的你一定会发现在构建用于开发环境的代码时很快就能完成,但在构建用于线上的代码时构建一直卡在一个时间点迟迟没有反应,其实卡住的这个时候就是在进行代码压缩。
由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多。
遇到这种情况可以改用 ParallelUglifyPlugin 插件,当 webpack 有多个 JS 文件需要输出和压缩时,原本会使用 UglifyJsPlugin 去一个个挨着压缩再输出, 但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJsPlugin 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。
压缩 CSS 代码的话,因为使用到 extract-text-webpack-plugin 插件将代码从 js 中分离出来,可以通过 optimize-css-assets-webpack-plugin 插件进行压缩,详细的配置项可以看:optimize-css-assets-webpack-plugin
输入分析:
上面讲了很多优化方式,但是无法面面俱到,对此你需要对打包输出结果做分析,以确认下一步的优化思路。这里推荐两个打包分析工具:
简单地介绍下第二种方式,接入方式很简单:
当你启动 webpack 时候,会唤起浏览器弹出 treemap,通过分析图可以清楚地看到:
结语
本文更多地介绍一些思路,详细的优化步骤可以自己去尝试,你肯定会有更多的收获的!
参考链接: