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

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

内容简介:现代 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' 是否是同一个东西 - 所以保持小心。


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

查看所有标签

猜你喜欢:

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

Python 3面向对象编程

Python 3面向对象编程

[加]Dusty Phillips(达斯帝•菲利普斯) / 肖鹏、常贺、石琳 / 电子工业出版社 / 2015-6 / 79.00元

Python 是一种面向对象的解释型语言,面向对象是其非常重要的特性。《Python 3面向对象编程》通过Python 的数据结构、语法、设计模式,从简单到复杂,从初级到高级,一步步通过例子来展示了Python 中面向对象的概念和原则。 《Python 3面向对象编程》不是Python 的入门书籍,适合具有Python 基础经验的开发人员阅读。如果你拥有其他面向对象语言的经验,你会更容易理解......一起来看看 《Python 3面向对象编程》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

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

HEX HSV 互换工具