zoukankan      html  css  js  c++  java
  • 【译文】使用webpack提高网页性能优化

    这篇文章原文来自https://developers.google.com/web/fundamentals/performance/webpack/。

    说是译文其实更像是笔者做的笔记,如有错误之处请指正。

    减小前端资源大小

    使用Production mode(webpack4限定)

    webpack提供了mode属性,你可以设置该属性为‘development’或者‘production’。

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

    更多阅读链接:

    压缩资源(webpack3)

    可以从bundle-level和loader-specific options两个方面进行。

    Bundle-level minification

    webpack4:在mode为production时自动进行。

    webpack3:使用UglifyJS plugin

    Loader-specific options

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

    定义NODE_ENV=production

    这个适用于webpack3,webpack4使用mode=production就行

    一些库会判断NODE_ENV的参数然后判断是否打印warnings等。

    在webpack4中,你可以这么写:

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

    在webpack3中,你需要使用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(),
    ],
    };

    使用这俩种方法都会让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.');
    }

    而minifier会删除所有的if分支,因为production!==production总是false,这段代码永远不会执行。

    使用ES模块

    ES模块就是ES6引入的export和import模块。

    当你使用ES modules的时候,webpack可以使用tree-shaking。tree-shaking可以让打包出来的文件在遍历依赖树的时候,移除没有使用的模块。

    1. 只用导出模块的其中之一的时候:
    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();
    1. webpack明白commentRestEndpoint没有使用,同时不生成导出模块语句
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    ;
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;
    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })
    1. ​minifier会移除不需要的变量
    1
    2
    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})

    Warning:不要不经意将ES modules 编译为 CommonJS 模式

    如果用了babel-preset-env或者babel-preset-es2015,请检查presets设置。一半来说ES的import和export会转换为CommonJS的require和module.exports。传入{modules:false}来制止这个行为。

    【译者注】如果需要兼容不支持ES6的浏览器还是不能使用这个特性吧。

    优化图片

    使用url-lodaer,svg-url-loaderimage-webpack-loader优化图片资源。

    url-loader的好处是能将根据你配置的小于某个大小的图片,使用Base64 data url的形式将图片嵌入javascript。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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
    7
    8
    9
    // 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…'
    //如果图片小于10kb,imageUrl会包含编码后的图片...
    // → If image.png is larger than 10 kB, the loader will create a new file,
    // and `imageUrl` will include its url: `/2fcd56a1920be.png`
    //如果图片大于10kb,loader会创建一个新的文件

    svg-url-loader和url-loader功能差不多,不过它将文件编码为URL encoding而不是Base64。这点对于SVG图片是比较好的,并且更高效。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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,
    },
    },
    ],
    },
    };

    svg-webpack-loader针对IE浏览器呦iesafe:true的选项

    优化依赖

    JavaScript的有些依赖包的大小比较大,但是我们往往只需要其中的一部分功能。

    比如Lodash大概72KB,但是我们只用到了其中的20个方法的话,可能有65KB的代码是浪费的。

    另一个例子是Moment.js,它223KB的文件中有大约170KB的文件是多语言本地化库,如果你不需要支持那么多语言的话,那么就需要减负。

    优化这些库的方法Google在github开了一个项目,点这里查看。

    使用module concatenation(模块连接)

    当你打包代码的时候,webpack将import和export的代码包裹在不同函数中。

    1
    2
    3
    4
    5
    6
    7
    8
    // index.js
    import {render} from './comments.js';
    render();
    // comments.js
    export function (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 (data, target) {
    console.log('Rendered!');
    }
    })

    以往是需要从中分离出CommonJS/AMD模块的,但是这个加入了一些多余的代码。

    Webpack2加入了ES模块系统,而webpack3使用了module concatenation,看看它是怎么减少代码的:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 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 (data, target) {
    console.log('Rendered!');
    }
    // CONCATENATED MODULE: ./index.js
    render();
    })

    可以看到导出的模块提升到了引入的部分之前,这样之前代码的1部分被移除了。

    在webpack4中,我们可以通过optimization.concatenateModules选项:

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

    在webpack3中,使用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(),
    ],
    };

    注意:这个方法虽然好但是没有设置为默认是有原因的,它会增加打包时间并且破坏热更新机制,所以它只能在production模式中使用。

    使用externals如果你同时使用webpack和non-webpack的代码

    如果项目比较大的话,可能有些代码使用webpack编译而有些没有。比如视频播放页面,视频组件由webpack编译,而别的没有。

    这时,如果两部分代码都使用了某个依赖的话,你可以使用externals属性,将webpack编译的代码中这些依赖移除,而使用非webpack部分代码所用依赖。

    依赖在window(全局)可用

    1
    2
    3
    4
    5
    6
    7
    module.exports = {
    externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    },
    };
    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提高网页性能优化td class="code">
    module.exports = {
    output: { libraryTarget: 'amd' },
    externals: {
    'react': { amd: '/libraries/react.min.js' },
    'react-dom': { amd: '/libraries/react-dom.min.js' },
    },
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    1
    2
    // bundle.js (beginning)
    define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

    善用缓存

    另一个提升应用性能的方式是使用缓存,缓存可以将部分数据保存在本地,避免重复下载。

    使用版本号和缓存http头

    1.浏览器对文件采用很长时间的缓存

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

    如果你不熟悉Cache-Control的话,可以看看这篇文章

    2.通过改变文件名(新的版本号)的方式进行重新加载

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

    通过webpack,你可以使用[chunkhash]来改变文件名。

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

    为了在客户端能找到文件名改变了的文件,可以使用HtmlWebpackPlugin或者WebpackManifestPlugin

    HtmlWebpackPlugin用起来比较简单,但是缺乏灵活性。它生成一个包含所有资源的HTML。如果你的服务端逻辑不复杂的话,用它就够了:

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

    WebpackManifestPlugin是一个更灵活的方法,在构建过程中它生成一个映射有hash和没hash文件的JSON。可以通过这个JSON文件在服务端找到相应资源。

    1
    2
    3
    4
    // manifest.json
    {
    "bundle.js": "bundle.8e0d62a03.js"
    }

    Extract dependencies and runtime into a separate file(依赖和运行时代码分开)

    针对依赖

    应用的依赖通常相较于业务代码改变的频率更低。如果你将它们分开到不同的文件,浏览器会分别对它们进行缓存,这样可以避免每次更新不变的依赖。

    在webpack术语中,从应用中分离出的文件称为chunks(块)

    1.替换输出文件为[name].[chunkname].js

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

    2.将entry替换为对象

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

    3.在webpack中,增加optimization.splitChunks.chunks:'all'

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

    在webpack3中,使用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文件夹的文件打包到vendor.[chunkhash].js中。

    Webpack runtime code

    只把vendor代码提出来是不够的,如果在业务代码中改变内容,vendor文件的hash仍然会改变。

    这种情况的出现主要是在vendor代码中,有一小块的代码包含了chunk ids和相关文件的映射,这部分代码会改变。

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

    webpack将这部分运行环境代码放在最后生成的chunk中,在我们的例子里就是vendor,造成了vendor变化。

    【译者注】我在vue-cli生成的配置文件里看到了它的解决方案。

    1
    2
    3
    4
    5
    6
    7
    > // extract webpack runtime and module manifest to its own file in order to
    > // prevent vendor hash from being updated whenever app bundle is updated
    > new webpack.optimize.CommonsChunkPlugin({
    > name: 'manifest',
    > minChunks: Infinity
    > }),
    >

    >

    在webpack4中,可以用optimization.runtimeChunk选项:

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

    在webpack中,增加一个空的chunk就可以。(这也是vue-cli的做法)

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

    将webpack执行环境放在html中减少额外HTTP请求

    因为执行环境(runtime)代码很小,将它放在html中可以节省HTTP请求。

    将:

    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>
    • 如果你使用HtmlWebpackPlugin生成HTML模版

    使用InlineSourcePlugin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
    module.exports = {
    plugins: [
    new HtmlWebpackPlugin({
    // Inline all files which names start with “runtime~” and end with “.js”.
    // That’s the default naming of runtime chunks
    inlineSource: 'runtime~.+.js',
    }),
    // This plugin enables the “inlineSource” option
    new InlineSourcePlugin(),
    ],
    };
    • 如果使用服务端逻辑生成HTML模版

    在webpck4中:

    1. 增加

      WebpackManifestPlugin

      得到生成的文件名

      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. 将代码加入HTML中,比如 Node和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>
      `);
      });

    或者Webpack3:

    1. 将环境代码的文件名固定:

      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. 在服务端中放入内容:

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

    懒加载

    通过import()code-solitting实现。

    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()引入的文件,webpack会将它分离成chunk,当使用到的时候才会加载。

    如果使用了Babel,可能会遇上语法错误,你需要使用syntax-dynamic-import插件

    通过router分割代码

    这块现代框架已经做的比较好了,可以参考:

    监控并分析应用

    上几章讲了如何优化,这章主要讲了如何分析应用到底有哪部分过于臃肿,追踪webpack的打包过程。

    追踪打包大小

    监控应用大小,你可以使用 webpack-dashboard 在开发过程 和 bundlesize 在CI中。

    分析为什么bundle过大

    你可以用 webpack-bundle-analyzer这个工具分析。

    这篇可以在原文中看,这里就不作为重点描述了

  • 相关阅读:
    CCF2014123集合竞价(C语言版)
    CCF2016092火车购票
    CCF2013123最大的矩形(C语言版)
    CCF2015122消除类游戏(C语言版)
    CCF2014032窗口(C语言)
    CCF2016093炉石传说(C语言版)
    go module 获取码云私有仓库代码
    centos7 编译安装 redis-6.0.5
    goland2019.2破解方法
    mac下protobuf配置记录
  • 原文地址:https://www.cnblogs.com/lijianming180/p/12275651.html
Copyright © 2011-2022 走看看