性能文章>使用 webpack 进行 Web 性能优化(一)>

使用 webpack 进行 Web 性能优化(一)转载

6月前
216514

现代 Web 应用程序通常使用打包工具来创建文件(脚本、样式表等)“包”,这些文件经过优化、缩小,用户可以在更短的时间内下载。 在使用 webpack 进行 Web 性能优化此文中,我们将介绍如何使用 webpack 有效地优化站点资源。 这可以帮助用户更快地加载您的网站并与之交互。

WebPack LOGO - WebPack性能优化 - HeapDump性能社区

Webpack 是当今最流行的大包工具之一。 利用其优化现代代码的功能,将代码拆分为关键和非关键部分并去除未使用的代码(仅举几例优化)可以确保您的应用程序具有最低的网络和处理成本。

Webpack Code-splitting in Bundle Buddy - WebPack性能优化 - HeapDump性能社区

 

通过 webpack 减小前端大小

优化应用程序时要做的第一件事就是使其尽可能小。 这是使用 webpack 执行此操作的方法。

Webpack 4 引入了新的模式标志。 您可以将此标志设置为“开发”或“生产”以提示 webpack 您正在为特定环境构建应用程序:

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

 

确保在构建生产应用程序时启用生产模式。 这将使 webpack 应用优化,如缩小、删除库中仅用于开发的代码等。

 

Minification 是指通过删除多余的空格、缩短变量名等来压缩代码。 像这样:

// 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;
}

// 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 minification 和 Loader-specific选项。 它们应该同时使用。

Bundle-level minification在编译后压缩整个包。 它的工作原理如下:

  1. 假设以下是你编写的代码:
     
    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
     
  2. Webpack 会将其编译成大致如下:
     
    // 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. 压缩器将其压缩为大致如下:
     
    // 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 级别的压缩是自动启用的——无论是在生产模式下还是在没有生产模式下。 它在后台使用 UglifyJS 缩小器。 (如果您需要禁用缩小,只需使用开发模式或将 false 传递给 optimization.minimize 选项。)

在 webpack 3 中,需要直接使用 UglifyJS 插件。 该插件与 webpack **在一起; 要启用它,请将其添加到配置的插件部分:

// webpack.config.js
const webpack = require('webpack');

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

缩小代码的第二种方法是特定于加载程序的选项(加载程序是什么)。 使用加载器选项,您可以压缩压缩器无法压缩的内容。 例如,当您使用 css-loader 导入 CSS 文件时,该文件会被编译为字符串:

/* comments.css */
.comment {
  color: black;
}

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

压缩器无法压缩此代码,因为它是一个字符串。 为了缩小文件内容,我们需要配置加载器来做到这一点:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};

 

指定 NODE_ENV=production

另一种减少前端大小的方法是将代码中的 NODE_ENV 环境变量设置为值生产。

库读取 NODE_ENV 变量以检测它们应该在哪种模式下工作 - 在开发或生产模式中。 一些库的行为基于此变量而有所不同。 例如,当 NODE_ENV 未设置为生产时,Vue.js 会进行额外的检查并打印警告:

// vue/dist/vue.runtime.e**.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.');
}
// …

React 的工作方式类似——它加载包含警告的开发构建:

// 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' 选项来移除它们:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

在 webpack 3 中,使用 DefinePlugin 代替:

// 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 选项和 DefinePlugin 的工作方式相同——它们用指定的值替换所有出现的 process.env.NODE_ENV。 使用上面的配置:

  1. Webpack 会将所有出现的 process.env.NODE_ENV 替换为“production”:
     
    // vue/dist/vue.runtime.e**.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.');
    }
     
     
    // vue/dist/vue.runtime.e**.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' 总是错误的,并且插件知道这些分支中的代码永远不会执行:
     
    // vue/dist/vue.runtime.e**.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.');
    }
     
     
    // vue/dist/vue.runtime.e**.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
     

减小前端大小的下一个方法是使用 ES 模块。

当你使用 ES 模块时,webpack 可以进行 tree-shaking。 Tree-shaking 是**器遍历整个依赖关系树,检查使用了哪些依赖项,并删除了未使用的依赖项。 所以,如果你使用 ES 模块语法,webpack 可以消除未使用的代码:

  1. 您编写了一个包含多个导出的文件,但应用程序只使用其中一个:
     
    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
     
  2. Webpack 理解没有使用 commentRestEndpoint 并且不会在包中生成单独的导出点:
     
    // 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. 压缩器删除未使用的变量:
     
    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
     

如果库是用 ES 模块编写的,这甚至适用于库。

 

图片占页面大小的一半以上。 虽然它们不像 JavaScript 那样重要(例如,它们不会阻塞渲染),但它们仍然占用了很大一部分带宽。 在 webpack 中使用 url-loader、svg-url-loader 和 image-webpack-loader 来优化它们。

url-loader 将小的静态文件内联到应用程序中。 如果没有配置,它会获取一个传递的文件,将其放在已编译的包旁边并返回该文件的 url。 但是,如果我们指定 limit 选项,它会将小于此限制的文件编码为 Base64 数据 url 并返回此 url。 这会将图像内联到 JavaScript 代码中并保存 HTTP 请求:

 
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files **aller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};
 
 
// index.js
import imageUrl from './image.png';
// → If image.png is **aller 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`
 
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          // Inline files **aller than 10 kB (10240 bytes)
          limit: 10 * 1024,
          // Remove the quotes from the url
          // (they’re unnecessary in most cases)
          noquotes: true,
        },
      },
    ],
  },
};
 // 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',
      },
    ],
  },
};

加载器的默认设置已经很好了——但如果你想进一步配置它,请参阅插件选项。 要选择要指定的选项,请查看 Addy O**ani 的出色图像优化指南。

 

JavaScript 平均大小的一半以上来自依赖项,而其中的一部分可能是不必要的。

例如,Lodash(从 v4.17.4 开始)将 72 KB 的压缩代码添加到包中。 但是,如果您只使用它的 20 种方法,那么大约 65 KB 的压缩代码将无济于事。

另一个例子是 Moment.js。 它的 2.19.1 版本需要 223 KB 的压缩代码,这是巨大的——2017 年 10 月,页面上 JavaScript 的平均大小为 452 KB。然而,其中 170 KB 是本地化文件。 如果您不将 Moment.js 与多种语言一起使用,那么这些文件将毫无目的地使包膨胀。

所有这些依赖关系都可以轻松优化。 我们在 GitHub 存储库中收集了优化方法 - 看看吧

 

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

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

// 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 模块相互隔离。 但是,这增加了每个模块的大小和性能开销。

Webpack 2 引入了对 ES 模块的支持,与 CommonJS 和 AMD 模块不同,这些模块可以在不使用函数包装的情况下进行**。 而 webpack 3 使这种**成为可能——通过模块连接。 以下是模块连接的作用:

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

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

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// 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();

})

看到不同? 在普通包中,模块 0 需要模块 1 的渲染。通过模块连接,require 被简单地替换为所需的函数,并且模块 1 被删除。 该**包的模块更少 - 模块开销也更少!

要开启此行为,请在 webpack 4 中启用 optimization.concatenateModules 选项:

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

在 webpack 3 中,使用 ModuleConcatenationPlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

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

你可能有一个大型项目,其中一些代码是用 webpack 编译的,而一些代码不是。 就像一个视频托管网站,播放器小部件可能是用 webpack 构建的,而周围的页面可能不是:

用externals处理webpack和非webpack代码 - WebPack性能优化 - HeapDump性能社区
(一个完全随机的视频托管网站)

如果两段代码具有共同的依赖关系,您可以共享它们以避免多次下载它们的代码。 这是通过 webpack 的 externals 选项完成的——它用变量或其他外部导入替换模块。

如果依赖项在 window 中有效

如果您的非 webpack 代码依赖于在窗口中作为变量可用的依赖项,请将依赖项名称别名为变量名称:

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
  },
};

使用这个配置,webpack 不会** react 和 react-dom 包。 相反,它们将被替换为以下内容:

// 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;
})

如果您的非 webpack 代码没有将依赖项暴露到 window 中,事情就会变得更加复杂。 但是,如果非 webpack 代码将这些依赖项作为 AMD 包使用,您仍然可以避免两次加载相同的代码。

为此,请将 webpack 代码编译为 AMD 包并将模块别名为库 URL:

// webpack.config.js
module.exports = {
  output: { libraryTarget: 'amd' },

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

Webpack 会将包包装到 define() 中,并使其依赖于这些 URL:

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

如果非 webpack 代码使用相同的 URL 来加载其依赖项,那么这些文件将只加载一次——额外的请求将使用加载器缓存。

注意:Webpack 只替换那些与外部对象的键完全匹配的导入。 这意味着,如果您编写 import React from 'react/umd/react.production.min.js',该库将不会从包中排除。 这是合理的——webpack 不知道 import 'react' 和 import 'react/umd/react.production.min.js' 是否相同——所以要小心。

 

延伸阅读:使用 webpack 进行 Web 性能优化(二)

点赞收藏
分类:标签:
风之石
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】抛砖系列之前端性能分析工具

【全网首发】抛砖系列之前端性能分析工具

从零开始搞监控系统(1)——SDK

从零开始搞监控系统(1)——SDK

从零开始搞监控系统(2)——存储和分析

从零开始搞监控系统(2)——存储和分析

从零开始搞监控系统(3)——性能监控

从零开始搞监控系统(3)——性能监控

Sentry 错误监控实战

Sentry 错误监控实战

前端构建效率优化之路

前端构建效率优化之路

4
1