作者 Addy OsmaniIvan Akulov

原文 https://developers.google.com/web/fundamentals/performance/webpack/

PS. 在 20180211 笔者翻译过一次,当时也没有完全理解和使用文中提到的优化项,近期工作中因为用到 Webpack 4.x 对生产环境进行打包,加深了一些理解,本译文对原有译文补充的 Webpack 4 内容,同时对原译文进行了校对和一些细节措辞的修改。

目录

  1. Instroduction 介绍
  2. Decrease Front-end Size 减少前端体积
    1. Use the production mode (webpack 4 only) 使用生产模式(仅用于 webpack 4)
      1. Further reading 扩展阅读
  3. Enable minification 开启最小化
    1. Bundle-level minification bundle 级别的最小化
    2. Loader-specific options 特定的 Loader 配置
      1. Further reading
  4. Specify NODE_ENV=production 明确生产环境信息
    1. Further Reading 扩展阅读
  • Use ES Modules 使用 ES 模块
    1. Futher reading 扩展阅读
  • Optimize images 优化图片
    1. Further reading 扩展阅读
  • Optimize dependencies 优化依赖
  • Enable module concatenation for ES modules (aka scope hoisting) 为 ES modles 开启模块连接
    1. Further reading 扩展阅读
  • Use externals if you have both webpack and non-webpack code 如果代码中包含 webpack 和非 webpack 的代码要使用 externals
    1. 如果依赖是挂载到 window 上的情况
    2. 如果依赖是当做 AMD 包被加载的情况
      1. Further reading 扩展阅读
  • Summing up 总结
  • Make use of long-term caching 利用好长时缓存
    1. Use bundle versioning and cache headers 使用 bundle 版本和缓存头信息
      1. Further reading 扩展阅读
  • Extract dependencies and runtime into a separate file 将依赖和运行环境代码提取到一个单独的文件
    1. Dependencies 依赖
    2. Webpack runtime code 运行时代码
      1. Further reading 扩展阅读
  • Inline webpack runtime to save an extra HTTP request 内联 webpack runtime 节省额外的 HTTP 请求
    1. 如果使用 HtmlWebpackPlugin 来生成 HTML
    2. 如果使用自己的定制服务逻辑来生成 HTML
      1. Webpack 4
      2. Webpack 3
  • Lazy-load code that you don’t need right now 懒加载
    1. Further reading 扩展阅读
  • Split the code into routes and pages 拆分代码到路由和页面中
    1. For single-page apps 对于单页面应用
    2. For traditional multi-page apps 对于传统的多页面应用
    3. Further reading 扩展阅读
  • Make module ids more stable 使用稳定的 module ids
    1. Further reading 扩展阅读
  • Summing up
  • Monitor and analyze the app 监控并分析
    1. Keep track of the bundle size 跟踪打包的体积
      1. webpack-dashboard
      2. bundlesize
        1. Find out the maximum sizes 找出最大体积
        2. Enable bundlesize 启用 bundlesize
      3. Further reading 扩展阅读
    2. Analyze why the bundle is so large 分析 bundle 为什么这么大
    3. Summing up 小结
  • Conclusion
  • Instroduction 介绍

    作者 Addy Osmani

    现代 Web 应用经常用到 bunding tool 来创建生产环境的打包文件(例如脚本、样式等等),打包文件是需要优化压缩最小化,同时能够被用户更快地加载。在这篇文章中,我们将会利用 webpack 来贯穿说明如何进行高效地优化网站资源。这能帮助用户更快地加载你的应用同时获得更好的体验。

    webpack-logo

    webpack 是当今最流行的打包工具之一,深入地利用它的特点去优化代码,将脚本拆分成不同的部分,同时剔除无用代码将能够保证你的应用维持最小的带宽和进程消耗。

    code-splitting

    Note: 我们创建了一个练习的应用来演示下面这些优化的描述。尽力抽更多的时间来练习这些 tips webpack-training-project

    让我们从现代 web 应用中最耗费资源之一的 Javascript 开始。


    Decrease Front-end Size 减少前端体积

    作者 Ivan Akulov

    当你正在优化一个应用时,首要事情就是尽可能地将它体积的减小。下面我们就来看看通过 webpack 如何做到减小前端体积。

    Use the production mode (webpack 4 only) 使用生产模式(仅用于 webpack 4)

    Webpack 4 介绍了一种新的模式,你可以将其设置成 developmentproduction 用于告诉 Webpack 你正在为不同的环境打包:

    1
    2
    3
    4
    // webpack.config.js
    module.exports = {
    mode: 'production',
    };

    当你正在为你的应用用于生产环境编译打包时要确定开启了 production 模式。这样就帮助 webpack 开启类似压缩最小化代码、去除依赖库中开发环境代码等其他的优化项。

    Further reading 扩展阅读

    Note: 笔者也翻译了另外一篇介绍新增 mode 的文章,感兴趣可以点击链接

    Enable minification 开启最小化

    Note: 大部分只针对 webpack 3 如果你正在使用 webpack 4 生产模式打包,bundle 级别的最小化功能已经开启 - 你只需要配置对应 loader 选项即可

    最小化就是通过去除多余空格、缩短变量名等方式压缩代码。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Original code
    function map(array, iteratee) {
    let index = -1;
    const length = array == null ? 0 : array.length;
    const result = new Array(length);

    while (++index < length) {
    result[index] = iteratee(array[index], index, array);
    }
    return result;
    }

    1
    2
    // Minified code
    function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

    Webpack 支持两种方式最小化代码:bundle-level 最小化 和 loader-specific options。他们可以同时使用。

    Bundle-level minification bundle 级别的最小化

    Bundle-level 最小化功能可以在编译完成后压缩整个 bundle。下面来看下它是如何工作的:

    1.原始代码如下:

    1
    2
    3
    4
    5
    // comments.js
    import './comments.css';
    export function render(data, target) {
    console.log('Rendered!');
    }

    2.Webpack 编译后的内容大概是下面这个样子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bundle.js (part of)
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony export (immutable) */ __webpack_exports__["render"] = render;
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
    __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);

    function render(data, target) {
    console.log('Rendered!');
    }

    3.最小化之后的代码大概是下面这个样子:

    1
    2
    3
    // minified bundle.js (part of)
    "use strict";function t(e,n){console.log("Rendered!")}
    Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)

    Webpack 4 中,bundle 级别的的最小化是自动开启的 - 同时在生产模式下、没有启用 bundle-level 都会开启。它是利用 UglifyJS 引擎来进行最小化的。(如果你需要禁用最小化,仅仅设置开发模式或者设置 optimization.minimizefalse。)

    Webpack 3 中,你需要直接使用 UglifyJS 插件。该插件是 webpack 提供的;开启并设置插件选项即可:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    const webpack = require('webpack');

    module.exports = {
    plugins: [
    new webpack.optimize.UglifyJsPlugin(),
    ],
    };

    Note: 在 webpack 3 中,UglifyJS 插件不能编译 ES2015+(ES6) 的代码,这就意味着你在代码中使用 classes, arrow function 或者其他新特性时,不能将他们编译成 ES5的代码,插件会抛错。
    如果你需要编译这些新语法,就要用到 uglifyjs-webpack-plugin package,他也是在 webpack 中捆绑一起的,但是版本更新,并且可以编译 ES2015+ 的代码。

    Loader-specific options 特定的 Loader 配置

    最小化代码的第二步就是利用特定的 loader 配置。配置这些 loader,你可以压缩那些不能被最小化的部分。举个例子,当你使用 css-loader 引入一个 css 文件时,文件会被编译成一个字符串:

    1
    2
    3
    4
    /* comments.css */
    .comment {
    color: black;
    }

    1
    2
    3
    // minified bundle.js (part of)
    exports=module.exports=__webpack_require__(1)(),
    exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);

    这部分内容由于是字符串并没有被最小化。于是我们需要配置对应的 loader 选项来达到最小化的目的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // webpack.config.js
    module.exports = {
    module: {
    rules: [
    {
    test: /\.css$/,
    use: [
    'style-loader',
    { loader: 'css-loader', options: { minimize: true } },
    ],
    },
    ],
    },
    };
    Further reading

    Specify NODE_ENV=production 明确生产环境信息

    Note: 仅在 webpack 3 中生效,如果使用生产模式 webpack 4 打包,NODE_ENV=production 优化项已经开启,就可以直接跳过此小结

    减小前端体积的另外一个方法就是在代码中将 NODE_ENV 环境变量设置为 production

    Libraries 会读取 NODE_ENV 变量判断他们应该在那种模式下工作 - 开发模式 or 生成模式。很多库会基于这个变量有不同的表现。举个例子,当NODE_ENV没有设置成production,Vue.js 会做额外的检查并且输出一些警告:

    1
    2
    3
    4
    5
    6
    // vue/dist/vue.runtime.esm.js
    // …
    if (process.env.NODE_ENV !== 'production') {
    warn('props must be strings when using array syntax.');
    }
    // …

    React 也是类似 - 开发模式下 build 带有一些警告:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // react/index.js
    if (process.env.NODE_ENV === 'production') {
    module.exports = require('./cjs/react.production.min.js');
    } else {
    module.exports = require('./cjs/react.development.js');
    }

    // react/cjs/react.development.js
    // …
    warning$3(
    componentClass.getDefaultProps.isReactClassApproved,
    'getDefaultProps is only used on classic React.createClass ' +
    'definitions. Use a static property named `defaultProps` instead.'
    );
    // …

    这些检查和警告通常在生产环境下是不必要的,但是他们仍然保留在代码中并且会增加库的体积。

    Webpack 4 中增加 optimization.nodeEnv: 'production' 选项即可剔除掉它们:

    1
    2
    3
    4
    5
    6
    7
    // webpack.config.js (for webpack 4)
    module.exports = {
    optimization: {
    nodeEnv: 'production',
    minimize: true,
    },
    };

    Webpack 3 中则使用 DefinePlugin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     // webpack.config.js (for webpack 3)
    const webpack = require('webpack');

    module.exports = {
    plugins: [
    new webpack.DefinePlugin({
    'process.env.NODE_ENV': '"production"',
    }),
    new webpack.optimize.UglifyJsPlugin(),
    ],
    };

    optimization.nodeEnv: 'production' 选项和 DefinePlugin 插件采用相同的方式来解决这个问题 - 这个方式就是他们将 process.env.NODE_ENV 替换成特定的值,下面的配置可以说明:

    1.Webpack 会将所有 process.env.NODE_ENV 替换成 "production"

    1
    2
    3
    4
    5
    6
    7
    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
    name = camelize(val);
    res[name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
    warn('props must be strings when using array syntax.');
    }

    1
    2
    3
    4
    5
    6
    7
    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
    name = camelize(val);
    res[name] = { type: null };
    } else if ("production" !== 'production') {
    warn('props must be strings when using array syntax.');
    }

    2.与此同时最小化工具会移除掉所有 if 的条件分支 - 由于 "production" !== 'production' 永远会返回 false,这样分支内的代码就永远不会执行:

    1
    2
    3
    4
    5
    6
    7
    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
    name = camelize(val);
    res[name] = { type: null };
    } else if ("production" !== 'production') {
    warn('props must be strings when using array syntax.');
    }

    1
    2
    3
    4
    5
    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
    name = camelize(val);
    res[name] = { type: null };
    }
    Further Reading 扩展阅读

    Use ES Modules 使用 ES 模块

    下面这个方式利用 ES modules 减小前端体积。

    当你使用 ES module,webpack 有能力去做 tree-shaking。Tree-shaking 贯穿了整个依赖树,检查哪些依赖被使用,同时移除掉无用依赖。因此,如果你使用 ES module 方式的时候,webpack 帮你可以排除掉无用代码:

    1.一个有多个 export 的文件,但是 app 只需要其中一个:

    1
    2
    3
    4
    5
    6
    7
    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';

    // index.js
    import { render } from './comments.js';
    render();

    2.Webpack 分析 commentRestEndPoint 没有被用到,就不会在一个 bundle 中生成单独的 export:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;

    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })

    3.最小化工具就会移除掉无用变量:

    1
    2
    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})

    如果他们都是有 ES module 编写,就是与一些库并存时也是生效的。

    Note: 在 webpack 中,tree-shaking 没有 minifier 是无法生效的。 webpack 仅仅移除了没有被用到的 export 变量;UglifyJSPlugin才会移除无用代码。所以如果你编译打包时没有使用 minifier,打包后体积并不会更小。你也可以不一定使用这个插件。其他最小化的插件也支持移除 dead code(例如:Babel Minify plugin or Google Closure Compiler plugin

    Warning: 不要将 ES module 编译到 CommonJS 中。 如果你使用 Babel babel-preset-env or babel-preset-es2015,检查一下当前的配置。默认情况下, ES import and export to CommonJS require and module.exports。通过设置 option 来禁止掉Pass the { modules: false } option

    Futher reading 扩展阅读

    Optimize images 优化图片

    图片基本会占局页面一半以上体积。虽然它们不像 JavaScript 那么重要(比如它们不会阻止页面渲染),但图片仍然会占用掉一大部分带宽。可以利用 url-loadersvg-url-loaderimage-webpack-loader 来进行优化。

    url-loader 允许将小的静态文件打包进 app。没有配置的话,他需要通过传递文件,将它放在编译后的打包 bundle 内并返回一个这个文件的 url。然而,如果我们注明 limit 选项,它将会编码成更小的文件 base64 url 并返回这个 url。这样将图片放在 Javascript 代码中,可以节省 HTTP 的请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // webpack.config.js
    module.exports = {
    module: {
    rules: [
    {
    test: /\.(jpe?g|png|gif)$/,
    loader: 'url-loader',
    options: {
    // Inline files smaller than 10 kB (10240 bytes)
    limit: 10 * 1024,
    },
    },
    ],
    }
    };
    1
    2
    3
    4
    5
    6
    // index.js
    import imageUrl from './image.png';
    // → If image.png is smaller than 10 kB, `imageUrl` will include
    // the encoded image: 'data:image/png;base64,iVBORw0KGg…'
    // → If image.png is larger than 10 kB, the loader will create a new file,
    // and `imageUrl` will include its url: `/2fcd56a1920be.png`

    Note: 内联图片减少了独立请求的数量,这是很好的方式(even with HTTP/2),但是会增加 bundle下载和转换的时间和内存的消耗。一定要确保不要嵌入超大图片或者较多的图片 - 否则增加的 bundle 的时间将会掩盖做成内联图片的收益。

    svg-url-loaderurl-loader类似 - 都是将使用 URL encoding encode 文件。这对对于 SVG 图片很奏效 - 因为 SVG 文件是文本,encoding 在体积上更有效率:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // webpack.config.js
    module.exports = {
    module: {
    rules: [
    {
    test: /\.svg$/,
    loader: 'svg-url-loader',
    options: {
    // Inline files smaller than 10 kB (10240 bytes)
    limit: 10 * 1024,
    // Remove the quotes from the url
    // (they’re unnecessary in most cases)
    noquotes: true,
    },
    },
    ],
    },
    };

    Note: svg-url-loader 拥有改善 IE 浏览器支持的 options,但是在其他浏览器中更糟糕。如果你需要兼容 IE 浏览器,设置 iesafe: true 选项

    image-webpack-loader压缩图片使之变小。它支持 JPG,PNG,GIF 和 SVG,因为我们将会使用它所有类型。

    这个 loader 不会将图片嵌入在应用内,因此它必须与url-loadersvg-url-loader配合使用。避免复制粘贴到相同的 rules 中(一个用于 JPG/PNG/GIF 图片,另一个用于 SVG 图片),我们来使用enforce: pre作为单独的一个 rule 涵盖这个 loader:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // webpack.config.js
    module.exports = {
    module: {
    rules: [
    {
    test: /\.(jpe?g|png|gif|svg)$/,
    loader: 'image-webpack-loader',
    // This will apply the loader before the other ones
    enforce: 'pre',
    },
    ],
    },
    };

    默认 loader 设置就已经可以满足需求了 - 但如果你想要深入配置,请查看 the plugin options。为了选择哪些 options 需要明确,可以查看 Addy Osmani 的 guide on image optimization

    Further reading 扩展阅读

    Optimize dependencies 优化依赖

    平均一半以上的 Javascript 体积大小来源于依赖包,并且这些可能都不是必要的。

    举一个例子来说,Lodash(v4.17.4)增加了最小化代码的 72KB 大小到 bundle 中。但是如果你仅仅用到它的20个方法,大约 65 KB 代码没有用处。

    另外一个例子就是 Moment.js。 V2.19.1版本最小化后有 223KB,体积巨大 - 截至2017年10月一个页面内的 Javascript 平均体积是 452KB。但是,本地文件的体积占 170KB。如果你没有用到 多语言版 Moment.js,这些文件都会没有目的地使 bundle 更臃肿。

    所有这些依赖都可以被轻易优化。我们在 Github repo 收集了优化的建议,check it out

    Enable module concatenation for ES modules (aka scope hoisting) 为 ES modles 开启模块连接

    Note: 如果你在使用生产模式下的 webpack 4,modules concatention 已经开启,可以直接跳过本小节。

    当你构建 bundle 时,webpack 将每一个 module 封装进 function 中:

    1
    2
    3
    4
    5
    6
    7
    8
    // index.js
    import {render} from './comments.js';
    render();

    // comments.js
    export function render(data, target) {
    console.log('Rendered!');
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // bundle.js (part  of)
    /* 0 */
    (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
    Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();

    }),
    /* 1 */
    (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    __webpack_exports__["a"] = render;
    function render(data, target) {
    console.log('Rendered!');
    }

    })

    在以前,这么做是使 CommonJS/AMD modules 互相分离所必须的。但是,这会增加体积并且性能表现堪忧。

    Webpack 2 介绍了 ES modules 的支持,不像 CommonJS 和 AMD modules 一样,而是能够不用将每一个 module 用 function 封装起来。同时 Webpack 3 利用ModuleConcatenationPlugin完成这样一个 bundle,下面是例子:

    1
    2
    3
    4
    5
    6
    7
    8
    // index.js
    import {render} from './comments.js';
    render();

    // comments.js
    export function render(data, target) {
    console.log('Rendered!');
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // Unlike the previous snippet, this bundle has only one module
    // which includes the code from both files
    // 与前面的代码不同,这个 bundle 只有一个 module,同时包含两个文件

    // bundle.js (part of; compiled with ModuleConcatenationPlugin)
    /* 0 */
    (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

    // CONCATENATED MODULE: ./comments.js
    function render(data, target) {
    console.log('Rendered!');
    }

    // CONCATENATED MODULE: ./index.js
    render();

    })

    看到区别了吗?在这个 bundle 中, module 0 需要 module 1 的 render 方法。使用 ModuleConcatenationPluginrequire被直接简单的替换成 require 函数,同时 module 1 被删除删除掉了。这个 bundle 拥有更少的 modules,就有更少的 modules 损耗!

    Webpack 4 中开启这个功能,启用 optimization.concatenateModules 选项即可:

    1
    2
    3
    4
    5
    6
    // webpack.config.js (for webpack 4)
    module.exports = {
    optimization: {
    concatenateModules: true,
    },
    };

    webpack 3 中,使用 ModuleConcatenationPlugin 插件:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js (for webpack 3)
    const webpack = require('webpack');

    module.exports = {
    plugins: [
    new webpack.optimize.ModuleConcatenationPlugin(),
    ],
    };

    Note:想要知道为什么这个功能不是默认启用?Concatenating modules 很棒, 但是他会增加编译的时间同时破坏 module 的热更新。这就是为什么只在生产环境中启用的原因了。

    Further reading 扩展阅读

    Use externals if you have both webpack and non-webpack code 如果代码中包含 webpack 和非 webpack 的代码要使用 externals

    你可能拥有一个体积庞大的工程,其中一部分代码可以使用 webpack 编译,而有一些代码又不能。比如一个视频网站,播放器的 widget 可能通过 webpack 编译,但是其周围页面区域可能不是:

    video-hosting

    如果两部分代码有相同的依赖,你可以共享这些依赖以便减少重复下载耗时。the webpack’s externals option就干了这件事 - 它用变量或者外部引用来替代 modules。

    如果依赖是挂载到 window 上的情况

    如果你的非 webpack 代码依靠这些依赖,它们是挂载 window 上的变量,可以将依赖名称 alias 成变量名:

    1
    2
    3
    4
    5
    6
    7
    // webpack.config.js
    module.exports = {
    externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    },
    };

    利用这个配置,webpack 将不会打包 reactreact-dom 包。取而代之,他们会被替换成下面这个样子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bundle.js (part of)
    (function(module, exports) {
    // A module that exports `window.React`. Without `externals`,
    // this module would include the whole React bundle
    module.exports = React;
    }),
    (function(module, exports) {
    // A module that exports `window.ReactDOM`. Without `externals`,
    // this module would include the whole ReactDOM bundle
    module.exports = ReactDOM;
    })

    如果依赖是当做 AMD 包被加载的情况

    如果你的非 webpack 代码没有将依赖暴露挂载到 window 上,这就更复杂了。但是如果非 webpack 代码使用 AMD 包的形式消费了这些依赖,你仍然可以避免重复的代码加载两次。

    具体如何做呢?将 webpack 代码编译成一个 AMD module 同时别名成一个库的 URLs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // webpack.config.js
    module.exports = {
    output: { libraryTarget: 'amd' },

    externals: {
    'react': { amd: '/libraries/react.min.js' },
    'react-dom': { amd: '/libraries/react-dom.min.js' },
    },
    };

    Webpack 将会把 bundle 包装进 define()同时让它依赖于这些URLs:

    1
    2
    // bundle.js (beginning)
    define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

    如果非 webpack 代码使用相同的 URLs 加载依赖,这些文件将会加载一次 - 多余的请求会使用缓存。

    Note:webpack 只是替换那些 externals 对象中的准确匹配的 keys 的引用。这意味着如果你的代码这样写import React from 'react/umd/react.production.min.js',这个库是不会被 bundle 排除掉的。这是因为 - webpack 并不知道 import 'react'import 'react/umd/react.production.min.js' 是同一个库,这样比较谨慎。

    Further reading 扩展阅读

    Summing up 总结

    • Enable the production mode if you use webpack 4 如果使用 webpack 4 开启生产模式
    • Minimize your code with the bundle-level minifier and loader options 使用 bundle 级别最小化 和 loader 选项来最小化你的代码
    • Remove the development-only code by replacing NODE_ENV with production 通过将 NODE_ENV 替换成 production 来移除开发期间代码
    • Use ES modules to enable tree shaking 启用 tree shaking
    • Compress images 压缩图片
    • Apply dependency-specific optimizations 开启依赖优化
    • Enable module concatenation 开启 module 连接
    • Use externals if this makes sense for you 如果有效果的话可以使用 externals

    Make use of long-term caching 利用好长时缓存

    作者 Ivan Akulov

    在做完优化应用体积之后的下一步提升应用加载时间的就是缓存。在客户端中使用缓存作为应用的一部分,这样会在每一次请求中减少重新下载的次数。

    Use bundle versioning and cache headers 使用 bundle 版本和缓存头信息

    做缓存通用的解决办法:

    1.告诉浏览器缓存一个文件很长时间(比如一年)

    1
    2
    # Server header
    Cache-Control: max-age=31536000

    Note:如果你不熟悉 Cache-Control 做了什么,你可以看一下 Jake Archibald 的精彩博文 on caching best practices

    2.当文件改变需要强制重新下载时去重命名这些文件

    1
    2
    3
    4
    5
    <!-- Before the change -->
    <script src="./index-v15.js"></script>

    <!-- After the change -->
    <script src="./index-v16.js"></script>

    这些方法可以告诉浏览器下载这些 JS 文件,将其缓存起来。浏览器将只会在文件名发生改变时才会请求网络(或者缓存失效的情况也会请求)。

    使用 webpack,也可以做同样的事,但可以使用版本号来解决,需要明确这个文件的 hash 值。使用 [chunkhash] 可以将 hash 值包含进文件名中:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    module.exports = {
    entry: './index.js',
    output: {
    filename: 'bundle.[chunkhash].js',
    // → bundle.8e0d62a03.js
    },
    };

    Note: webpack 可能会生成不同的 hash 即使 bundle 相同 - 比如你重名了了一个文件或者重新在不同的操作系统下编译了一个 bundle。 This is a bug.
    如果你需要将文件名发送给客户端,也可以使用 HtmlWebpackPlugin 或者 WebpackManifestPlugin

    HtmlWebpackPlugin 使用起来很简单,但灵活性有一些欠缺。编译时,插件会生成一个 HTML 文件,这其中包括所有的编译后的资源文件。如果你的业务逻辑不复杂,这就非常适合你:

    1
    2
    3
    4
    <!-- index.html -->
    <!doctype html>
    <!-- ... -->
    <script src="bundle.8e0d62a03.js"></script>

    WebpackManifestPlugin 更灵活一些,它可以帮助你解决业务负责的部分。编译时它会生成一个 JSON 文件,这文件保存这没有 hash 值文件与有 hash 文件之间的映射。服务端利用这个 JSON 可以识别出那个文件有效:

    1
    2
    3
    4
    // manifest.json
    {
    "bundle.js": "bundle.8e0d62a03.js"
    }
    Further reading 扩展阅读

    Extract dependencies and runtime into a separate file 将依赖和运行环境代码提取到一个单独的文件

    Dependencies 依赖

    App 依赖通常情况下趋向于比实际 app 内代码中更少的变化。如果你将他们移到独立的文件中,浏览器将可以把他们独立缓存起来 - 同时不会每次 app 代码改变时重新下载。

    Key Term: 在 webpack 的技术中,利用 app 代码拆分文件被称为 chunks。我们后面会用到这个名词。

    为了将依赖包提取到单独的 chunk 中,下面分为三步:

    1.使用 [name].[chunkname].js 替换output的文件名:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // webpack.config.js
    module.exports = {
    output: {
    // Before
    filename: 'bundle.[chunkhash].js',
    // After
    filename: '[name].[chunkhash].js',
    },
    };

    当 webpack 构建应用时,它会用一个带有 chunk 的名称来替换 [name]。如果没有添加 [name] 部分,我们不得不通过 chunks 之间的 hash 区别来比较他们的区别 - 那就太困难了!

    2.将 entry 转成一个对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // webpack.config.js
    module.exports = {
    // Before
    entry: './index.js',
    // After
    entry: {
    main: './index.js',
    },
    };

    在这段代码中,”main” 对象是一个 chunk 的名字。这个名字将会被步骤 1 里面的 [name]代替。

    目前为止,如果你构建一个 app,chunk 就会包括整个 app 的代码 - 就像我们没有做这些步骤一样。但是很快就会产生变化。

    3.在 Webpack 4 中,在配置中增加 optimization.splitChunks.chunks: 'all' 即可:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js (for webpack 4)
    module.exports = {
    optimization: {
    splitChunks: {
    chunks: 'all',
    }
    },
    };

    这个选项会开启智能代码拆分。使用这个功能,webpack 将最小化和 Gzip 前大于 30KB 的代码提取出额外的 vendor 代码。它同时也会提取出 common 代码 - 这些代码在打包多个 bundles 会起到作用。(例如:通过路由拆分应用)。

    Webpack 3 中,使用 CommonsChunkPlugin 插件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // webpack.config.js (for webpack 3)
    module.exports = {
    plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    // A name of the chunk that will include the dependencies.
    // This name is substituted in place of [name] from step 1
    name: 'vendor',

    // A function that determines which modules to include into this chunk
    minChunks: module => module.context &&
    module.context.includes('node_modules'),
    }),
    ],
    };

    插件将包括全部 node_modules 路径下的 modules 同时将他们移到一个单独的文件中,这个文件被称为 vendor.[chunkhash].js

    完成了上面的步骤,每一次 build 都会生成两个文件。浏览器会将他们单独缓存 - 以便代码发生改变时重新下载。

    1
    2
    3
    4
    5
    6
    7
    $ webpack
    Hash: ac01483e8fec1fa70676
    Version: webpack 3.8.1
    Time: 3816ms
    Asset Size Chunks Chunk Names
    ./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
    ./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor

    Webpack runtime code 运行时代码

    不幸的是,仅仅抽取 vendor 是不够的。如果你试图在应用代码中修改一些东西:

    1
    2
    3
    4
    5
    6
    // index.js



    // E.g. add this:
    console.log('Wat');

    你会注意到 vendor 的也会改变:

    1
    2
                               Asset   Size  Chunks             Chunk Names
    ./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor

    1
    2
                                Asset   Size  Chunks             Chunk Names
    ./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor

    这因为 webpack 打包时,一部分 modules 的代码,拥有 a runtime - 管理模块执行一部分代码。当你将代码拆分成多个文件时,这小部分代码在 chunk ids 和 匹配的文件之间开始了一个映射:

    1
    2
    3
    4
    // vendor.e6ea4504d61a1cc1c60b.js
    script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
    }[chunkId] + ".js";

    Webpack 将最新生成的 chunk 包含在这个 runtime 内,这个 chunk 就是我们代码中的 vendor。与此同时每一次任何 chunk 的修改,即使这一小部分代码也改变,也会导致整个 vendor chunk 改变。

    为了解决这个问题,我们将 runtime 转义到一个独立的文件中,在 Webpack 4 中,开启 optimization.runtimeChunk 选项:

    1
    2
    3
    4
    5
    6
    // webpack.config.js (for webpack 4)
    module.exports = {
    optimization: {
    runtimeChunk: true,
    },
    }

    Webpack 3中,通过 CommonsChunkPlugin 创建一个额外的空的 chunk:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // webpack.config.js (for webpack 3)
    module.exports = {
    plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',

    minChunks: module => module.context &&
    module.context.includes('node_modules'),
    }),

    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime',

    // minChunks: Infinity means that no app modules
    // will be included into this chunk
    minChunks: Infinity,
    }),
    ],
    };

    完成这一部分改变,每一次 build 都将生成三个文件:

    1
    2
    3
    4
    5
    6
    7
    8
    $ webpack
    Hash: ac01483e8fec1fa70676
    Version: webpack 3.8.1
    Time: 3816ms
    Asset Size Chunks Chunk Names
    ./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
    ./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
    ./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

    将他们反过来顺序添加到 index.html 中,你就搞定了:

    1
    2
    3
    4
    <!-- index.html -->
    <script src="./runtime.79f17c27b335abc7aaf4.js"></script>
    <script src="./vendor.26886caf15818fa82dfa.js"></script>
    <script src="./main.00bab6fd3100008a42b0.js"></script>
    Further reading 扩展阅读

    Inline webpack runtime to save an extra HTTP request 内联 webpack runtime 节省额外的 HTTP 请求

    为了做的更好,我们可以尽力把 webpack runtime 内联在 HTML 请求里。下面举例:

    1
    2
    <!-- index.html -->
    <script src="./runtime.79f17c27b335abc7aaf4.js"></script>

    这样做:

    1
    2
    3
    4
    <!-- index.html -->
    <script>
    !function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
    </script>

    这个 runtime 很小,内联它可以帮助你节省 HTTP 请求(尤其对 HTTP/1 重要;但是在 HTTP/2 就没有那么重要了,但是仍能够提高效率)。

    下面就来看看如何做。

    如果使用 HtmlWebpackPlugin 来生成 HTML

    如果使用 HtmlWebpackPlugin 来生成 HTML 文件,InlineChunkWebpackPlugin 就足够了。

    如果使用自己的定制服务逻辑来生成 HTML

    Webpack 4

    1.增加 WebpackManifestPlugin 插件已知运行时 chunk:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');

    module.exports = {
    plugins: [
    new ManifestPlugin(),
    ],
    };

    插件就会生成一个下面这样的文件:

    1
    2
    3
    4
    // manifest.json
    {
    "runtime~main.js": "runtime~main.8e0d62a03.js"
    }

    2.将这些内容嵌入到 runtime chunk 中。例如:使用 Node.js 和 Express:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');

    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');

    app.get('/', (req, res) => {
    res.send(`

    <script>${runtimeContent}</script>

    `);
    });
    Webpack 3

    1.将 runtime 名称改成静态的明确的文件名:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // webpack.config.js (for webpack 3)
    module.exports = {
    plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime',
    minChunks: Infinity,
    filename: 'runtime.js',
    // → Now the runtime file will be called
    // “runtime.js”, not “runtime.79f17c27b335abc7aaf4.js”
    }),
    ],
    };

    2.嵌入到 runtime.js 内容。比如:Node.js 和 Express

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');

    app.get('/', (req, res) => {
    res.send(`

    <script>${runtimeContent}</script>

    `);
    });

    Lazy-load code that you don’t need right now 懒加载

    通常情况下,一个页面有或多或少的重要部分:

    • 如果你在 YouTube 上加载一个视频页面,相比评论区域你更在乎视频区域。这就是视频要比评论区域重要。
    • 如果你在一个新闻网站打开一个报道,相比广告区域你更关心文章的内容。这就是文字比广告更重要。

    在这些案例中,通过仅下载最重要的部分,懒加载剩余区域能够提升最初的加载性能。使用 the import() functioncode-splitting 解决这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // videoPlayer.js
    export function renderVideoPlayer() { … }

    // comments.js
    export function renderComments() { … }

    // index.js
    import {renderVideoPlayer} from './videoPlayer';
    renderVideoPlayer();

    // …Custom event listener
    onShowCommentsClick(() => {
    import('./comments').then((comments) => {
    comments.renderComments();
    });
    });

    import()明确表示你期望动态地加载独立的 module。当 webpack 看到 import('./module.js')时,他就会将这个 module 移到独立的 chunk 中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ webpack
    Hash: 39b2a53cb4e73f0dc5b2
    Version: webpack 3.8.1
    Time: 4273ms
    Asset Size Chunks Chunk Names
    ./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
    ./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
    ./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
    ./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

    并且只在代码执行到 import() 才会下载。

    这将会让 main bundle 更小,提升初始加载的时间。更重要的是改进缓存 - 如果你修改 main chunk 的代码,其他部分的 chunk 也不会受影响。

    Note: 如果使用 Babel 编译代码,你会因为 Babel 还不认识 import() 而遇到语法错误抛出来。可以使用 syntax-dynamic-import 解决这个错误。

    Further reading 扩展阅读

    Split the code into routes and pages 拆分代码到路由和页面中

    如果你的应用拥有多个路由或者页面,但是代码中只有单独一个 JS 文件(一个单独的 main chunk),这看起来你正在每一个请求中节省额外的 bytes 带宽。举个例子,当用户正在访问你网站的首页:

    site-home-page

    他们并不需要加载另外不同的页面上渲染文章标题的的代码 - 但是他们还是会加载到这段代码。更严重的是如果用户经常只访问首页,同时你还经常改变渲染文章标题的代码,webpack 将会对整个 bundle 失效 - 用户每次都会重复下载全部 app 的代码。

    如果我们将代码拆分到页面里(或者单页面应用的路由里),用户就会只下载对他有意义的代码。更好的是,浏览器也会更好地缓存代码:当你改变首页的代码时,webpack 只会让相匹配的 chunk 失效。

    For single-page apps 对于单页面应用

    通过路由拆分带页面引用,使用 import()(看看 “Lazy-load code that you don’t need right now”这部分)。如果你在使用一个框架,现在已经有成熟的方案:

    For traditional multi-page apps 对于传统的多页面应用

    通过页面拆分传统多页面应用,可以使用 webpack 的 entry points 。如果你的应用有三种页面:主页、文章页、用户账户页,那就分厂三个 entries:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    module.exports = {
    entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
    },
    };

    对于每一个 entry 文件,webpack 将构建出独立的依赖树,并且声称一个 bundle,它将通过 entry 来只包括用到的 modules:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ webpack
    Hash: 318d7b8490a7382bf23b
    Version: webpack 3.8.1
    Time: 4273ms
    Asset Size Chunks Chunk Names
    ./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
    ./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
    ./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
    ./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
    ./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
    ./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime

    因此,如果仅仅是文章页使用 Lodashhomeprofile 的 bundle 将不会包含 lodash - 同时用户也不会在访问首页的时候下载到这个库。

    拆分依赖树也有缺点。如果两个 entry points 都用到了 loadash ,同时你没有在 vendor 移除掉依赖,两个 entry points 将包括两个重复的 lodash 。在 Webpack 4 中我们可以设置 optimization.splitChunks.chunks: 'all' 解决该问题:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js (for webpack 4)
    module.exports = {
    optimization: {
    splitChunks: {
    chunks: 'all',
    }
    },
    };

    这个选项可以开启智能拆分代码,webpack 将自动寻找 common code 并将其提取到一个单独的文件中。

    Webpack 3 可以使用CommonsChunkPlugin来解决这个问题 - 它会将通用的依赖转移到一个独立的文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // webpack.config.js (for webpack 3)
    module.exports = {
    plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    // A name of the chunk that will include the common dependencies
    name: 'common',

    // The plugin will move a module into a common file
    // only if it’s included into `minChunks` chunks
    // (Note that the plugin analyzes all chunks, not only entries)
    minChunks: 2, // 2 is the default value
    }),
    ],
    };

    随意使用minChunks的值来找到最优的选项。通常情况下,你想要它尽可能体积小,但它会增加 chunks 的数量。举个例子,3 个 chunk,minChunks 可能是 2 个,但是 30 个 chunk,它可能是 8 个 - 因为如果你把它设置成 2 ,过多的 modules 将会打包进一个通用文件中,文件更臃肿。

    Further reading 扩展阅读

    Make module ids more stable 使用稳定的 module ids

    当编译代码时,webpack 会分配给每一个 module 一个 ID。之后,这些 ID 就会被 require() 引用到 bundle 内部。你可以在编译输出的右侧在 moudle 路径之前看到这些 ID:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ webpack
    Hash: df3474e4f76528e3bbc9
    Version: webpack 3.8.1
    Time: 2150ms
    Asset Size Chunks Chunk Names
    ./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
    ./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
    ./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
    ./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

    1
    2
    3
    4
    5
    6
    [0] ./index.js 29 kB {1} [built]
    [2] (webpack)/buildin/global.js 488 bytes {2} [built]
    [3] (webpack)/buildin/module.js 495 bytes {2} [built]
    [4] ./comments.js 58 kB {0} [built]
    [5] ./ads.js 74 kB {1} [built]
    + 1 hidden module

    默认情况下,这些 ID 是使用计数器计算出来的(比如第一个 module 是 ID 0,第二个 moudle 就是 ID 1,以此类推)。这样的问题就在于当你新增一个 module 事,它会出现在原来 module 列表中的中间,改变后面所有 module 的 ID:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ webpack
    Hash: df3474e4f76528e3bbc9
    Version: webpack 3.8.1
    Time: 2150ms
    Asset Size Chunks Chunk Names
    ./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
    ./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
    ./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
    ./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
    [0] ./index.js 29 kB {1} [built]
    [2] (webpack)/buildin/global.js 488 bytes {2} [built]
    [3] (webpack)/buildin/module.js 495 bytes {2} [built]

    ↓ 我们增加一个新 module

    1
    [4] ./webPlayer.js 24 kB {1} [built]

    ↓ 现在看这里做了什么! comments.js 现在的 ID 由 4 变成了 5

    1
    [5] ./comments.js 58 kB {0} [built]

    ads.js 的 ID 由 5 变成 6

    1
    2
    [6] ./ads.js 74 kB {1} [built]
    + 1 hidden module

    这将使包含或依赖于具有更改ID的模块的所有块无效 - 即使它们的实际代码没有更改。在我们的代码中,_0_ 这个 chunk 和 main chunk 都会失效 - 只有 main 才应该失效。

    使用HashedModuleIdsPlugin插件改变module ID 如何计算来解决这个问题。它利用 module 路径的 hash 来替换掉计数器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ webpack
    Hash: df3474e4f76528e3bbc9
    Version: webpack 3.8.1
    Time: 2150ms
    Asset Size Chunks Chunk Names
    ./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
    ./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
    ./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
    ./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime

    1
    2
    3
    4
    5
    6
    7
    [3IRH] ./index.js 29 kB {1} [built]
    [DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
    [JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
    [LbCc] ./webPlayer.js 24 kB {1} [built]
    [lebJ] ./comments.js 58 kB {0} [built]
    [02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

    有了这个方法,只有你重命名或者删除这个 moudle 它的 ID 才会变化。新的 modules 不会因为 module ID 互相影响。

    启用这个插件,在配置中增加 plugins

    1
    2
    3
    4
    5
    6
    // webpack.config.js
    module.exports = {
    plugins: [
    new webpack.HashedModuleIdsPlugin(),
    ],
    };
    Further reading 扩展阅读

    Summing up

    • Cache the bundle and differentiate between versions by changing the bundle name 缓存 bundle 包并通过修改 bundle 名称来做版本差异
    • Split the bundle into app code, vendor code and runtime 将 bundle 拆分成 app 业务代码、vendor 代码、runtime 代码
    • Inline the runtime to save an HTTP request 将 runtime 代码内联节省 HTTP 请求
    • Lazy-load non-critical code with import 通过 import 懒加载非必要代码
    • Split code by routes/pages to avoid loading unnecessary stuff 通过路由或页面拆分阻止加载不必要代码

    Monitor and analyze the app 监控并分析

    作者 Ivan Akulov

    即使当你配置好你的 webpack 让你的应用尽可能体积较小的时候,跟踪这个应用就非常重要,同时了解里面包含了什么。除此之外,你安装一个依赖,它将让你的 app 增加两倍大小 - 但并没有注意到这个问题!

    这一部分就来讲解一些能够帮助你理解你的 bundle 的工具。

    Keep track of the bundle size 跟踪打包的体积

    在开发时可以使用webpack-dashboard和命令行bundlesize 来监控 app 的体积。

    webpack-dashboard

    webpack-dashboard可以通过依赖体积大小、进程和其他细节来改进 webpack 的输出。

    webpack-dashboard

    这个 dashborad 帮助我们跟踪大型依赖 - 如果你增加一个依赖,你就立刻能在 Modules section 始终看到它!

    启用这个功能,需要安装 webpack-dashboard 包:

    1
    npm install webpack-dashboard --save-dev

    同时在配置的 plugins 增加:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    const DashboardPlugin = require('webpack-dashboard/plugin');

    module.exports = {
    plugins: [
    new DashboardPlugin(),
    ],
    };

    或者如果正在使用基于 Express dev server 可以使用 compiler.apply()

    1
    compiler.apply(new DashboardPlugin());

    多尝试 dashboard 找出改进的地方!比如,在 modules section 滚动找到那个库体积过大,把它替换成小的可替代的库。

    bundlesize

    bundlesize 可以验证 webpack assets 不超过指定的大小。通过自动化 CI 就可以知晓 app 是否变的过于臃肿:

    bundlesize

    配置如下:

    Find out the maximum sizes 找出最大体积

    1.分析 app 尽可能减小体积,执行生产环境的 build。
    2.在package.json中增加bundlesize部分:

    1
    2
    3
    4
    5
    6
    7
    8
    // package.json
    {
    "bundlesize": [
    {
    "path": "./dist/*"
    }
    ]
    }

    3.使用npx执行bundlesize

    1
    npx bundlesize

    它就会将每一个文件的 gzip 压缩后的体积打印出来:

    1
    PASS  ./dist/icon256.6168aaac8461862eab7a.png:  10.89KB PASS./dist/icon512.c3e073a4100bd0c28a86.png:  13.1KB PASS./dist/main.0c8b617dfc40c2827ae3.js:  16.28KB PASS./dist/vendor.ff9f7ea865884e6a84c8.js:  31.49KB

    4.每一个体积增加10-20%,你将得到最大体积。这个10-20%的幅度可以让你像往常一样开发应用程序,同时警告你,当它的大小增长太多。

    Enable bundlesize 启用 bundlesize

    5.安装bundlesize开发依赖

    1
    npm install bundlesize --save-dev

    6.在package.json中的bundlesize部分,声明具体的最大值。对于某一些文件(比如图片),你可以单独根据文件类型来设置最大体积大小,而不需要根据每一个文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // package.json
    {
    "bundlesize": [
    {
    "path": "./dist/*.png",
    "maxSize": "16 kB",
    },
    {
    "path": "./dist/main.*.js",
    "maxSize": "20 kB",
    },
    {
    "path": "./dist/vendor.*.js",
    "maxSize": "35 kB",
    }
    ]
    }

    7.增加一个 npm 脚本来执行检查:

    1
    2
    3
    4
    5
    6
    // package.json
    {
    "scripts": {
    "check-size": "bundlesize"
    }
    }

    8.配置自动化 CI 来在每一次 push 时执行npm run check-size做检查。(如果你在 Github 上开发项目,直接可以使用integrate bundlesize with GitHub。)

    这就全部了!现在如果你运行npm run check-size或者 push 代码,你就会看到输出的文件是否足够小:

    bundlesize-output-success

    或者下面失败的情况

    bundlesize-output-failure

    Further reading 扩展阅读

    Analyze why the bundle is so large 分析 bundle 为什么这么大

    你想要深挖 bundle 内,看看里面具体哪些 module 占用多大空间。webpack-bundle-analyzer

    译者注:此处有视频,需要科学上网,请自行观看

    (Screen recording from github.com/webpack-contrib/webpack -bundle-analyzer)

    webpack-bundle-analyzer 可以扫描 bundle 同时构建一个查看内部的可视化窗口。使用这个可视化工具找到过大或者不必要的依赖。

    使用这个分析器,需要安装webpack-bundle-analyzer包:

    1
    npm install webpack-bundle-analyzer --save-dev

    在 config 中增加插件:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

    module.exports = {
    plugins: [
    new BundleAnalyzerPlugin(),
    ],
    };

    运行生产环境的 build 这个插件就会在浏览器中打开一个显示状态的页面。

    默认情况下,这个页面会显示语法分析后的文件体积(在 bundle 出现的文件)。您可能想比较 gzip 的大小,因为这更接近实际用户的体验;使用左边的边栏来切换尺寸。

    Note: 如果你使用 ModuleConcatenationPlugin,它可能在webpack-bundle-analyzer输出时合并一部分 module,使得报告小一些细节。如果你使用这个插件,在执行分析的时候需要禁用掉。

    下面是报告中需要看什么:

    • Large dependencies 大型依赖 为什么体积这么大?是否有更小的替代包(比如 Preact 替代 React)?用了全部代码(比如 Moment.js 包含大量的本地变量 that are often not used and could be dropped)?
    • Duplicated dependencies 重复依赖 是否在不同文件中看到过相同的库?(在 Webpack 4 中配置 optimization.splitChunks.chunks,或者在 Webpack 3中 使用 CommonsChunkPlugin 将他们移到一个通用文件内)亦或是在同一个库中 bundle 拥有多个版本?
    • Similar dependencies 相似依赖 是否存在有相似功能的相似库存在?(比如momentdate-fns 或者 lodashlodash-es)尽力汇总成一个。

    同样的,也可以看看 Sean Larkin 的文章 great analysis of webpack bundles

    Summing up 小结

    • Use webpack-dashboard and bundlesize to stay tuned of how large your app is
    • Dig into what builds up the size with webpack-bundle-analyzer

    Conclusion

    总结:

    • 剔除不必要的体积 把所有的代码都压缩最小化,剔除无用代码,增加依赖时保持谨慎小心。
    • 通过路由拆分代码 只在真正需要的时候才加载,其余部分做懒加载。
    • 缓存代码 应用程序某些部分代码更新频率低于其他部分代码,可以将这些部分拆分成文件,以便在必要时仅重新下载。
    • 跟踪体积大小 使用 webpack-dashboardwebpack-bundle-analyzer 监控你的 app。每隔几个月重新检查一下你的应用的性能。

    Webpack 不仅仅是一个帮助你更快创建 app 的工具。它还帮助使你的 app 成为 a Progressive Web App ,你的应用拥有更好的体验以及自动化的填充工具就像Lighthouse根据环境给出建议。

    不要忘记阅读 webpack docs - 里面提供了大量的优化相关的信息。

    多多练习 with the training app