减小前端大小

如何使用 webpack 尽可能减小应用的大小

优化应用时,首先要做的就是让其体积尽可能小 下面介绍了如何使用 Webpack 执行此操作。

使用生产模式(仅限 webpack 4)

Webpack 4 引入了新的 mode 标志。您可以设置 将此标志设置为 'development''production',以提示您正在构建的 Webpack 特定环境的应用:

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

在构建正式版应用时,请务必启用 production 模式。 这会使 webpack 应用优化,例如缩减大小、移除开发专用代码

深入阅读

启用缩减大小功能

缩减大小是指通过移除多余的空格、缩短变量名称和 依此类推。示例如下:

// 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 支持两种缩减代码的方法:软件包级缩减特定于加载程序的选项。这两种模式应同时使用。

软件包级缩减大小

软件包级缩减大小会在编译后压缩整个软件包。具体流程如下:

  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 中,在正式版中,系统会自动启用软件包级缩减大小 也不使用这种模式它使用 UglifyJS 缩减器 功能。(如果您需要停用缩减大小功能,只需使用开发模式 或者将 false 传递给 optimization.minimize 选项。)

在 webpack 3 中,您需要使用 UglifyJS 插件 。该插件与 webpack 捆绑在一起:如需启用,请将其添加到 plugins 部分:

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

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

特定于加载程序的选项

第二种缩减代码的方法是特定于加载器的选项( is)。借助加载程序选项,您可以 因为缩减器无法缩减大小例如,当您导入包含 css-loader,该文件将被编译为一个字符串:

/* 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 环境变量 设置为 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 的工作方式与之类似,它会加载包含警告的开发 build:

// 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 替换为指定的值。使用 config:

  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. 然后,Minifier会将所有此类对象 if 分支,因为 "production" !== 'production' 始终为 false, 并且插件理解这些分支中的代码永远不会执行:

    // 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 模块

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

当您使用 ES 模块时,webpack 能够执行摇树优化。“摇树优化”是指 遍历整个依赖项树,检查使用了哪些依赖项,并移除未使用的依赖项。因此, 如果您使用 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 中单独导出一个导出点:

    // 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 模块编写的,也是如此。

不过,您并不一定要使用 webpack 的内置缩减器 (UglifyJsPlugin)。 任何支持移除死代码的压缩工具 (例如 Babel Minify 插件) 或 Google Closure 编译器插件) 就可以做到这一点

深入阅读

优化图像

图片会带来不止 页面大小的一半。虽然他们 不像 JavaScript 那么重要(例如,它们不会阻塞渲染),但它们仍然占有很大一部分 带宽。使用url-loadersvg-url-loaderimage-webpack-loader进行优化 webpack。

url-loader 将小型静态文件内嵌到 应用。如果不进行配置,则获取传递的文件,将其放在已编译的 bundle 旁边,然后返回 该文件的网址。不过,如果我们指定 limit 选项,它会对小于或等于 Base64 数据网址,并返回此网址。这个 将图片内嵌到 JavaScript 代码中并保存 HTTP 请求:

// 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,
        },
      },
    ],
  }
};
// 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`

svg-url-loader 的工作方式与 url-loader 类似, 只不过它会对包含网址 编码,而不是 Base64 一个。这对于 SVG 图片非常有用,因为 SVG 文件只是纯文本,所以此编码 规模效益更高

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

image-webpack-loader 可压缩 它支持 JPG、PNG、GIF 和 SVG 图片,因此我们将将其用于所有这些类型。

此加载器不会将图片嵌入应用,因此它必须与 url-loadersvg-url-loader。要避免将其复制并粘贴到两个规则(一个用于 JPG/PNG/GIF 图片,另一个用于 一个是 SVG 格式),我们将将此加载器作为单独的规则添加到 enforce: 'pre' 中:

// 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 Osmani 提供的优质图片指南 优化

深入阅读

优化依赖项

JavaScript 平均大小有一半以上来自依赖项,而其中一部分可能 没必要这样做。

例如,Lodash(从 v4.17.4 开始)会在 bundle 中添加 72 KB 的缩减代码。但如果您只使用 比如 20 个方法,那么大约 65 KB 的压缩后代码将什么都不做。

另一个例子是 Moment.js。其 2.19.1 版需要 223 KB 的缩减代码,这个大小非常大: 网页的平均 JavaScript 大小在 10 月份为 452 KB 2017 年。然而,该大小的 170 KB 是本地化 文件。如果 您没有将 Moment.js 与多种语言搭配使用, 目的。

所有这些依赖项都可以轻松优化。我们收集了多种优化方法, GitHub 代码库 – 查看

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

在构建 bundle 时,webpack 将每个模块封装到一个函数中:

// 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 中的 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()
  ]
};

深入阅读

如果您同时拥有 webpack 和非 webpack 代码,请使用 externals

您可能有一个大型项目,其中一些代码是使用 webpack 编译的,而另一些代码则不是。点赞 一个视频托管网站(其中播放器微件可能是用网页包构建的), 可能不是:

<ph type="x-smartling-placeholder">
</ph> 视频托管网站的屏幕截图
(一个完全随机的视频托管网站)

如果这两段代码有共同的依赖项,那么您可以共享它们,以免下载其代码 。这是通过 Webpack 的 externals 实现的 选项 - 它会用变量或 其他外部导入

如果 window 中有依赖项

如果您的非 Webpack 代码依赖于可作为 window 中的变量提供的依赖项,请将别名设为 从依赖项名称更改为变量名称:

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

使用此配置时,Webpack 不会捆绑 reactreact-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;
})

如果依赖项作为 AMD 软件包加载

如果您的非 Webpack 代码不会向 window 公开依赖项,那么情况就会更加复杂。 不过,如果非 webpack 代码使用同一个代码,您仍然可以避免 依赖项作为 AMD 软件包

为此,请将 webpack 代码编译为 AMD 捆绑包,并将别名模块编译为库网址:

// 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() 中,并使其依赖于以下网址:

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

如果非 webpack 代码使用相同的网址加载其依赖项,则系统将加载这些文件 仅一次 – 其他请求将使用加载器缓存。

深入阅读

汇总

  • 如果您使用 webpack 4,请启用生产模式
  • 使用软件包级缩减器和加载器选项最大限度地缩减代码
  • 通过将 NODE_ENV 替换为 production 来移除开发专用代码
  • 使用 ES 模块启用摇树优化
  • 压缩图片
  • 应用依赖项专用优化
  • 启用模块串联
  • 如果需要,请使用 externals