摘要:使用要给项目构建接入动态链接库的思想,需要完成以下事情把网页依赖的基础模块抽离出来,打包到一个个多带带的动态链接库中去。接入已经内置了对动态链接库的支持,需要通过个内置的插件接入,它们分别是插件用于打包出一个个多带带的动态链接库文件。
webpack优化
查看所有文档页面:全栈开发,获取更多信息。优化开发体验原文链接:webpack优化,原文广告模态框遮挡,阅读体验不好,所以整理成本文,方便查找。
优化构建速度。在项目庞大时构建耗时可能会变的很长,每次等待构建的耗时加起来也会是个大数目。
缩小文件搜索范围
使用 DllPlugin
使用 HappyPack
使用 ParallelUglifyPlugin
优化使用体验。通过自动化手段完成一些重复的工作,让我们专注于解决问题本身。
使用自动刷新
开启模块热替换
优化输出质量优化输出质量的目的是为了给用户呈现体验更好的网页,例如减少首屏加载时间、提升性能流畅度等。 这至关重要,因为在互联网行业竞争日益激烈的今天,这可能关系到你的产品的生死。
优化输出质量本质是优化构建输出的要发布到线上的代码,分为以下几点:
减少用户能感知到的加载时间,也就是首屏加载时间。
区分环境
压缩代码
CDN 加速
使用 Tree Shaking
提取公共代码
按需加载
提升流畅度,也就是提升代码性能。
使用 Prepack
开启 Scope Hoisting
缩小文件搜索范围Webpack 启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。 在遇到导入语句时 Webpack 会做两件事情:
根据导入语句去寻找对应的要导入的文件。例如 require("react") 导入语句对应的文件是 ./node_modules/react/react.js,require("./util") 对应的文件是 ./util.js。
根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。
优化 loader 配置由于 Loader 对文件的转换操作很耗时,需要让尽可能少的文件被 Loader 处理。
在 Module 中介绍过在使用 Loader 时可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件。 为了尽可能少的让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。
以采用 ES6 的项目为例,在配置 babel-loader 时,可以这样:
module.exports = { module: { rules: [ { // 如果项目源码中只有 js 文件就不要写成 /.jsx?$/,提升正则表达式性能 test: /.js$/, // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启 use: ["babel-loader?cacheDirectory"], // 只对项目根目录下的 src 目录中的文件采用 babel-loader include: path.resolve(__dirname, "src"), }, ] }, };
你可以适当的调整项目的目录结构,以方便在配置 Loader 时通过 include 去缩小命中范围。优化 resolve.modules 配置
在 Resolve 中介绍过 resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。
resolve.modules 的默认值是 ["node_modules"],含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。
当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
module.exports = { resolve: { // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤 // 其中 __dirname 表示当前工作目录,也就是项目根目录 modules: [path.resolve(__dirname, "node_modules")] }, };优化 resolve.mainFields 配置
在 Resolve 中介绍过 resolve.mainFields 用于配置第三方模块使用哪个入口文件。
安装的第三方模块中都会有一个 package.json 文件用于描述这个模块的属性,其中有些字段用于描述入口文件在哪里,resolve.mainFields 用于配置采用哪个字段作为入口文件的描述。
可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境中,准对不同的运行环境需要使用不同的代码。 以 isomorphic-fetch 为例,它是 fetch API 的一个实现,但可同时用于浏览器和 Node.js 环境。 它的 package.json 中就有2个入口文件描述字段:
{ "browser": "fetch-npm-browserify.js", "main": "fetch-npm-node.js" }
isomorphic-fetch 在不同的运行环境下使用不同的代码是因为 fetch API 的实现机制不一样,在浏览器中通过原生的 fetch 或者 XMLHttpRequest 实现,在 Node.js 中通过 http 模块实现。
resolve.mainFields 的默认值和当前的 target 配置有关系,对应关系如下:
当 target 为 web 或者 webworker 时,值是 ["browser", "module", "main"]
当 target 为其它情况时,值是 ["module", "main"]
以 target 等于 web 为例,Webpack 会先采用第三方模块中的 browser 字段去寻找模块的入口文件,如果不存在就采用 module 字段,以此类推。
为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main 字段去描述入口文件的位置,可以这样配置 Webpack:
module.exports = { resolve: { // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤 mainFields: ["main"], }, };
使用本方法优化时,你需要考虑到所有运行时依赖的第三方模块的入口文件描述字段,就算有一个模块搞错了都可能会造成构建出的代码无法正常运行。优化 resolve.alias 配置
resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径。
在实战项目中经常会依赖一些庞大的第三方模块,以 React 库为例,安装到 node_modules 目录下的 React 库的目录结构如下:
├── dist │ ├── react.js │ └── react.min.js ├── lib │ ... 还有几十个文件被忽略 │ ├── LinkedStateMixin.js │ ├── createClass.js │ └── React.js ├── package.json └── react.js
可以看到发布出去的 React 库中包含两套代码:
一套是采用 CommonJS 规范的模块化代码,这些文件都放在 lib 目录下,以 package.json 中指定的入口文件 react.js 为模块的入口。
一套是把 React 所有相关的代码打包好的完整代码放到一个多带带的文件中,这些代码没有采用模块化可以直接执行。其中 dist/react.js 是用于开发环境,里面包含检查和警告的代码。dist/react.min.js 是用于线上环境,被最小化了。
默认情况下 Webpack 会从入口文件 ./node_modules/react/react.js 开始递归的解析和处理依赖的几十个文件,这会时一个耗时的操作。 通过配置 resolve.alias 可以让 Webpack 在处理 React 库时,直接使用多带带完整的 react.min.js 文件,从而跳过耗时的递归解析操作。
相关 Webpack 配置如下:
module.exports = { resolve: { // 使用 alias 把导入 react 的语句换成直接使用多带带完整的 react.min.js 文件, // 减少耗时的递归解析操作 alias: { "react": path.resolve(__dirname, "./node_modules/react/dist/react.min.js"), } }, };
优化 resolve.extensions 配置除了 React 库外,大多数库发布到 Npm 仓库中时都会包含打包好的完整文件,对于这些库你也可以对它们配置 alias。
但是对于有些库使用本优化方法后会影响到后面要讲的使用 Tree-Shaking 去除无效代码的优化,因为打包好的完整文件中有部分代码你的项目可能永远用不上。 一般对整体性比较强的库采用本方法优化,因为完整文件中的代码是一个整体,每一行都是不可或缺的。 但是对于一些工具类的库,例如 lodash,你的项目可能只用到了其中几个工具函数,你就不能使用本方法去优化,因为这会导致你的输出代码中包含很多永远不会执行的代码。
在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在。resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:
extensions: [".js", ".json"]
也就是说当遇到 require("./data") 这样的导入语句时,Webpack 会先去寻找 ./data.js 文件,如果该文件不存在就去寻找 ./data.json 文件,如果还是找不到就报错。
如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions 的配置也会影响到构建的性能。 在配置 resolve.extensions 时你需要遵守以下几点,以做到尽可能的优化构建性能:
后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把 require("./data") 写成 require("./data.json")。
相关 Webpack 配置如下:
module.exports = { resolve: { // 尽可能的减少后缀尝试的可能性 extensions: ["js"], }, };优化 module.noParse 配置
module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
在上面的 优化 resolve.alias 配置 中讲到多带带完整的 react.min.js 文件就没有采用模块化,让我们来通过配置 module.noParse 忽略对 react.min.js 文件的递归解析处理, 相关 Webpack 配置如下:
const path = require("path"); module.exports = { module: { // 独完整的 `react.min.js` 文件就没有采用模块化,忽略对 `react.min.js` 文件的递归解析处理 noParse: [/react.min.js$/], }, };
注意被忽略掉的文件里不应该包含 import 、 require 、 define 等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。
以上就是所有和缩小文件搜索范围相关的构建性能优化了,在根据自己项目的需要去按照以上方法改造后,你的构建速度一定会有所提升。
使用 DllPlugin要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:
把网页依赖的基础模块抽离出来,打包到一个个多带带的动态链接库中去。一个动态链接库中可以包含多个模块。
当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
为什么给 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会在重新编译,而是直接使用动态链接库中的代码。 由于动态链接库中大多数包含的是常用的第三方模块,例如 react、react-dom,只要不升级这些模块的版本,动态链接库就不用重新编译。
接入 WebpackWebpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:
DllPlugin 插件:用于打包出一个个多带带的动态链接库文件。
DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。
下面以基本的 React 项目为例,为其接入 DllPlugin,在开始前先来看下最终构建出的目录结构:
├── main.js ├── polyfill.dll.js ├── polyfill.manifest.json ├── react.dll.js └── react.manifest.json
其中包含两个动态链接库文件,分别是:
polyfill.dll.js 里面包含项目所有依赖的 polyfill,例如 Promise、fetch 等 API。
react.dll.js 里面包含 React 的基础运行环境,也就是 react 和 react-dom 模块。
以 react.dll.js 文件为例,其文件内容大致如下:
var _dll_react = (function(modules) { // ... 此处省略 webpackBootstrap 函数代码 }([ function(module, exports, __webpack_require__) { // 模块 ID 为 0 的模块对应的代码 }, function(module, exports, __webpack_require__) { // 模块 ID 为 1 的模块对应的代码 }, // ... 此处省略剩下的模块对应的代码 ]));
可见一个动态链接库文件中包含了大量模块的代码,这些模块存放在一个数组里,用数组的索引号作为 ID。 并且还通过 _dll_react 变量把自己暴露在了全局中,也就是可以通过 window._dll_react 可以访问到它里面包含的模块。
其中 polyfill.manifest.json 和 react.manifest.json 文件也是由 DllPlugin 生成出,用于描述动态链接库文件中包含哪些模块, 以 react.manifest.json 文件为例,其文件内容大致如下:
See the Pen react.manifest.json by whjin (@whjin) on CodePen.