【译】使用 webpack 进行 web 性能优化(一):减小前端资源大小

栏目: 编程工具 · 发布时间: 6年前

内容简介:现代 web 应用经常使用webpack 是当下最流行的打包工具之一。我们可以利用其特性来优化代码,通过代码拆分可以将脚本拆分为核心和非核心部分,并且去除无用的代码(这仅仅是一小部分的优化案例),从而确保你的应用具有最小的网络负担和处理成本。

现代 web 应用经常使用 打包工具 来创建生产环境的“打包”文件(脚本、样式等等),这些文件经过优化和压缩之后能够极快的被用户下载。在 使用 webpack 进行 web 性能优化 系列文章中,我们将介绍如何使用 webpack 高效的优化站点资源。这将会帮助用户更快的加载网站以及交互。

【译】使用 webpack 进行 web 性能优化(一):减小前端资源大小

webpack 是当下最流行的打包 工具 之一。我们可以利用其特性来优化代码,通过代码拆分可以将脚本拆分为核心和非核心部分,并且去除无用的代码(这仅仅是一小部分的优化案例),从而确保你的应用具有最小的网络负担和处理成本。

【译】使用 webpack 进行 web 性能优化(一):减小前端资源大小

受 Susie Lu 的 在 Bundle Buddy 中进行代码拆分 的启发。

:star:️ 注意:我们创建了一个可供练习的应用来演示这篇文章中讲到的内容。请充分利用它来练习这些技巧: webpack-training-project

让我们从现今应用中最耗费资源之一的 JavaScript 开始优化。

第一篇:减小前端资源大小

当你正在优化一个应用时,第一件事就是尽可能地减少它的大小。这里介绍如何利用 webpack 来实现。

使用生产模式(仅限 webpack4)

webpack 4 引入了 新的 mode 标志 。你可以将这个标志设置为 'development' 或者 'production' 来告诉 webpack 你正在为特定环境构建应用:

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

当构建生产环境的应用时,请确保你开启了 production 模式。 这将让 webpack 开启它的优化项,比如:缩小尺寸、移除库中只在开发者模式才有的代码等等。

扩展阅读

启用最小化

:star:️ 注意: 这些大部分只适用于 webpack 3。如果你在 webpack 4 中开启了 production 模式,bundle-level 最小化已经启用 – 你只需要启用loader 特定(loader-specific)的选项。

最小化尺寸是在你通过移除多余的空格、缩短变量的命名等方式压缩代码的时候进行的。例如这样:

// 原来的代码
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;
}
复制代码

// 最小化后的代码
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 特定的选项 。它们应该同时使用。

Bundle-level 最小化

当编译完成后,bundle-level 最小化功能会压缩整个 bundle。这里展示了它是如何工作的:

  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. minifier 大致会压缩成下面那样:

    // 最小化过的 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-level 最小化功能是自动开启的 – 无论是否在生产模式。它在底层使用的是 UglifyJS 最小化 。(如果你需要禁用最小化,只要使用开发模式或者将 optimization.minimize 选项设置为 false 。)

在 webpack 3 中,你需要直接使用 UglifyJS 插件 。这个插件是 webpack 自带的;将它添加到配置的 plugins 部分即可启用:

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

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

:star:️ 注意: 在 webpack 3 中,UglifyJS 插件不能编译版本超过 ES2015 (即 ES6) 的代码。这意味着如果你的代码使用了类、箭头函数或者其它新的语言特性,你不能将它们编译成 ES5 版本的代码, 否则插件将抛出一个错误。

如果你需要编译包含新的语法(的代码),使用 uglifyjs-webpack-plugin 插件。 这同样是 webpack 自带的插件,但是版本更新,并且可以编译 ES2015+ 的代码。

loader 特定(loader-specific)的选项

最小化代码的第二种方法是 loader 特定的选项(loader 是什么)。利用 loader 选项,你可以压缩 minifier 不能最小化的东西。例如,当你利用 css-loader 导入一个 CSS 文件时,该文件会被编译成一个字符串:

/* comments.css */  
.comment {  
  color: black;  
}  
复制代码

// 最小化后的 bundle.js (部分代码)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n  color: black;\r\n}",""]);
复制代码

Minifier 不能压缩该代码,因为它是一个字符串。为了最小化文件内容,我们需要像这样配置 loader:

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

扩展阅读

指定NODE_ENV=production

:star:️ 注意: 这只适用于 webpack 3。如果你在production 模式下使用 webpack 4, NODE_ENV=production 优化已启用 – 可自由选择地跳过该部分。

减少前端大小的另一种方法是在你的代码中将 NODE_ENV 环境变量 设置为 production

库会读取 NODE_ENV 变量以检测它们应该在哪个模式下工作 – 在开发或生产中。 有些库基于该变量而有不同的表现。例如,当 NODE_ENV 没有设置为 production ,Vue.js 会做额外的检查并打印警告:

// vue/dist/vue.runtime.esm.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 (基于 webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
}; 
复制代码

在 webpack 3 中,则使用 DefinePlugin 来替代:

// webpack.config.js (基于 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.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.');
    }
    复制代码

    // 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' 总是错误的,插件明白这些分支中的代码永远不会执行:

    // 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.');
    }
    复制代码

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
    复制代码

扩展阅读

使用 ES 模块(module)

减小前端尺寸的另一种方法是使用ES 模块。

当你使用 ES 模块, webpack 就可以进行 tree-shaking。Tree-shaking 是当 bundler 遍历整个依赖树时,检查使用了什么依赖,并移除无用的。所以,如果你使用了 ES 模块语法, webpack 可以去掉未使用的代码:

  1. 你写了一个带有多个 export 的文件,但是应用只使用它们其中的一个:

    // 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:

    // bundle.js (和 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 模块写成的库也是有效的。

:star:️ 注意: 在 webpack 中,tree-shaking 没有 minifier 是不会起作用的。Webpack 仅仅移除没有被用到的 export 变量;是 minifier 移除未使用的代码的。所以,如果你在没有使用 minifier 的情况下编译 bundle,是不会减小的。

然而,你不需要特定使用 webpack 内置的 minifier ( UglifyJsPlugin )。任意的 minifier 都支持移除无用代码(例如 Babel Minify pluginGoogle Closure Compiler plugin ) 都可以奏效。

:exclamation: 警告: 不要将 ES 模块编译为 CommonJS 模块。

如果你使用 Babel 的 babel-preset-envbabel-preset-es2015 , 检查它们预先的设置。默认情况下, 它们将 ES 的 importexport 转译为 CommonJS 的 requiremodule.exports 通过 { modules: false } 选项 来禁用它。

与 TypeScript 相同:记得在你的 tsconfig.json 中设置 { "compilerOptions": { "module": "es2015" } }

扩展阅读

优化图片

图片占页面大小的一半以上。 尽管它们不如 JavaScript 关键(例如,它们不会阻塞渲染),但仍然消耗了带宽的一大部分。可以在 webpack 中使用 url-loadersvg-url-loaderimage-webpack-loader 来优化它们。

url-loader 将小的静态文件内联进应用。没有配置的话,它需要通过传递文件,将它放在编译后的打包 bundle 内并返回一个这个文件的 url。然而,如果我们指定了 limit 选项,它会将文件编码成比无配置更小的Base64 的数据 url 并将该 url 返回。这样可以将图片内联进 JavaScript 代码中,并节省一次 HTTP 请求:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // 小于 10kB(10240字节)的内联文件
          limit: 10 * 1024,
        },
      },
    ],
  }
};
复制代码
// index.js
import imageUrl from './image.png';
// → 如果图片小于 10kB, `imageUrl` 将包含
// 编码后的图片: 'data:image/png;base64,iVBORw0KGg…'
// → 如果图片大于 10B,该 loader 将创建一个新文件,
// 并且 `imageUrl` 将会包含它的 url: `/2fcd56a1920be.png`
复制代码

:star:️ 注意: 内联图片减少了单独请求的数量,这是好的(即使通过 HTTP/2),但是增加了 bundle 和内存消耗的下载/解析时间。确保不要嵌入大的或者很多的图片,否则增加的 bundle 时间可能超过内联带来的好处。

svg-url-loader 的工作原理类似于 url-loader – 除了它利用URL encoding 而不是 Base64 对文件编码。对于 SVG 图片这是有效的 – 因为 SVG 文件恰好是纯文本,这种编码规模效应更加明显:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          // 小于 10kB(10240字节)的内联文件
          limit: 10 * 1024,
          // 移除 url 中的引号
          // (在大多数情况下它们都不是必要的)
          noquotes: true,
        },
      },
    ],
  },
};
复制代码

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

image-webpack-loader 会压缩检查到的所有图片。它支持 JPG、PNG、GIF 和 SVG 格式的图片,因此我们在碰到所有这些类型的图片都会使用它。

这个 loader 不能将图片嵌入应用,所以它必须和 url-loader 以及 svg-url-loader 一起使用。为了避免同时将它复制粘贴到两个规则中(一个针对 JPG/PNG/GIF 图片, 另一个针对 SVG ),我们使用 enforce: 'pre' 作为单独的规则涵盖在这个 loader:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // 这会应用该 loader,在其它之前
        enforce: 'pre',
      },
    ],
  },
};
复制代码

加载器的默认设置已经很好了 - 但是如果你想更进一步去配置它,参考 插件选项 。要选择指定选项,请查看 Addy Osmani 的图像优化指南。

扩展阅读

优化依赖

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

例如,Lodash (自 v4.17.4 版本起) 增加了 72KB 的最小化代码到 bundle 中。但是如果你仅仅用到它的 20 种方法,那么大约 65KB 的代码是无用的。

另一个例子是 Moment.js。2.19.1 版本有 223KB 大小,这是巨大的 - 截至 2017 年 10 月,一个页面的 JavaScript 平均体积是452 KB。然而,其中的 170KB 是 本地化文件 。如果你没有用到多语言版 Moment.js,这些文件都将毫无目的地使 bundle 更臃肿。

所有的这些依赖都可以轻易地优化。我们已经在 GitHub 仓库中收集了优化方法 - 来看一下 !

为 ES 模块启用模块串联(又称作用域提升)

:star:️ 注意: 如果在生产模式下使用,模块串联已经启用。自由地跳过该部分。

当你构建 bundle 时,webpack 将每个 module 包装进一个函数中:

// 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 module,它们可以在不将每个模块都封装进函数中的情况下进行打包。并且 webpack 3 使这样的捆绑变得可能 - 通过模块连接。这是模块连接的工作原理:

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

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

// 与前面的代码段不同,此包只有一个模块
// 它包含来自两个文件的代码

// bundle.js (部分; 通过 ModuleConcatenationPlugin 编译)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

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

  // 级联模块: ./comments.js
  function render(data, target) {
    console.log('Rendered!');
  }

  // 级联模块: ./index.js
  render();

})
复制代码

看到不同了吗?在普通绑定中,模块 0 需要模块 1 的 render 。使用模块连接, require 只需用所需要的功能替换,模块 1 就被移除了。bundle 拥有更小的模块 – 以及更少的模块开支!

要在 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(),
  ],
};
复制代码

:star:️ 注意: 想知道为什么默认不启用这个行为吗?连接模块是很棒, 但是它增加了构建时间并破坏了模块热替换 。这是为什么它只在生产下启用。

扩展阅读

使用 externals ,如果同时包含 webpack 和非 webpack 代码

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

【译】使用 webpack 进行 web 性能优化(一):减小前端资源大小

(完全随机的视频托管网站)

如果代码块有公共的依赖,你可以共享它们以避免多次下载其代码。这是通过 webpack 的 externals 选项 完成的 – 它通过变量或其它的额外导入来替换模块。

如果依赖在 window 中可获得

如果你的 non-webpack 代码依赖于某些依赖,这些依赖在 window 中可以作为变量获得,将依赖名别名为变量名:

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

通过这个配置, webpack 不会打包 reactreact-dom 包。相反,它们将被替换成下面这样的东西:

// bundle.js (part of)
(function(module, exports) {
  // 导出 `window.React` 的模块。 没有 `externals`,
  // 这个模块会包含整个的 React 包
  module.exports = React;
}),
(function(module, exports) {
  // 导出 `window.React` 的模块。 没有 `externals`,
  // 这个模块会包含整个的 ReactDOM 包
  module.exports = ReactDOM;
})
复制代码

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

如果你的 non-webpack 代码没有将依赖暴露于 window ,事情就变得更加复杂。然而,如果这些非 webpack 代码将这些依赖作为AMD 包,你仍然可以避免相同的代码加载两次。

具体如何做呢,将 webpack 代码编译成一个 AMD bundle ,同时将模块别名为库的 URLs:

// 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:

// bundle.js (开始)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
复制代码

如果 non-webpack 代码使用了相同的 URLs 来加载它的依赖,那么这些文件只会加载一次 - 额外的请求将使用加载器缓存。

:star:️ 注意: Webpack 仅替换那些明确匹配 externals 对象的键的导入。这意味着如果你编写 import React from 'react/umd/react.production.min.js' ,这个库不会从 bundle 中排除。这是合理的 - webpack 不知道 import 'react'import 'react/umd/react.production.min.js' 是否是同一个东西 - 所以保持小心。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

小圈子·大社交

小圈子·大社交

Paul Adams / 王志慧 / 人民邮电出版社 / 2013-1 / 29.00元

网络正在脱离以内容为核心构建的方式,转向以人为核心重新构建。这样深远的变革将影响我们制定商业策略、设计以及营销和广告的方式。 本书作者先后在谷歌和Facebook供职,对于社交网络有深入的研究和丰富的实战经验。他以学术界和工业界最新的调查研究为基础,阐述了人们如何通过社交圈子相互联系的规律,探讨了理念和品牌信息如何通过社交网络传播开来的过程。书中介绍了许多实际的例子,通过这些鲜活的实例,你将......一起来看看 《小圈子·大社交》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具