使用Webpack4优化Web性能

利用 Webpack 来优化 Web 性能属于加载性能优化 的一部分: ☛ Web Performance Optimization with webpack

本文目录:

  • 减少前端资源体积
  • 使用长期缓存
  • 监控和分析应用程序
  • 总结

一、减少前端资源体积

1、webpack 4 开启 production 模式

production 模式下 webpack 会对代码进行优化,如减小代码体积,删除只在开发环境用到的代码。

可以在 webpack 中指定:

module.exports = {
  mode: 'production' // 或 development
};

或者 package.json 中配置:

"scripts": {
    "dev": "webpack-dev-server --mode development --open --hot",
    "build": "webpack --mode production --progress"
}
2、压缩代码

使用 bundle-level minifier 和 loader options 压缩代码。

  • Bundle-level minification

Bundle-level 的压缩会在代码编译后对整个包进行压缩。

在 webpack 4 中,production 模式下会自动执行 bundle-level 的压缩,底层使用了 the UglifyJS minifier。(如果不想开启压缩,可以采用 development 模式或者设置 optimization.minimize 为 false)

  • Loader-specific options

通过 loader 层面的选项配置来对代码进行压缩,是为了压缩 bundle-level minifier 无法压缩的内容,比如,通过 css-loader 编译后的文件,会成为字符串,就无法被 minifier 压缩。因此,要进一步压缩文件内容,可进行如下配置:

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

当使用 ES 模块时, webpack 能够进行 tree-shaking。

tree-shaking 是指 bundler 遍历整个依赖关系树,检查使用了哪些依赖关系,并删除未使用的依赖关系。因此,如果使用ES模块语法,webpack 可以消除未使用的代码。

★ 注意:在 webpack 中,如果没有 minifier,tree-shaking 就无法工作。webpack 只删除不使用的导出语句,而 minifier 则会删除未使用的代码。因此,如果在编译时不使用 minifier,代码量并不会减小。(除了使用 wbpack 内置的 minifier,其它的插件如 Babel Minify plugin 也能对代码进行压缩)。

✘ 警告:不要意外地将 ES 模块编译成 CommonJS 模块。如果你使用 Babel 的时候,采用了 babel-preset-env 或者 babel-preset-es2015,请检查这些预置的设置。默认情况下,它们会将 ES 的导入和导出转换为 CommonJS 的 requiremodule.exports,可以通过传递 { modules: false } 选项来禁用它。

Introduction to ES Modules
一口(很长的)气了解 babel
➹ Webpack docs about tree shaking

4、压缩图片资源

针对具体的依赖项进行优化(dependency-specific optimization

图像占了页面大小的一半以上。虽然它们不像JavaScript那样重要(例如,它们不会阻塞呈现),但它们仍然占用了很大一部分带宽。在 webpack 中可以使用 url-loadersvg-url-loaderimage-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 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`

★ 注意:需要在增大代码体积和减少 HTTP 请求数之前进行权衡。

svg-url-loader 的工作原理与 url-loader 类似 — 只是它使用的是URL编码而不是Base64编码来编码文件。这对SVG图像很有用 — 因为SVG文件只是纯文本,这种编码更高效。

// webpack.config.js
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-url-loader 有一些选项可以改进Internet Explorer的支持,但会使其他浏览器的内联更加糟糕。如果需要支持此浏览器,请应用 iesafe: 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',
      },
    ],
  },
};
5、优化第三方依赖

JavaScript 的大小平均有一半以上来自依赖项,而其中的一部分可能是不必要的。我们可以对这些依赖的库进行优化➡️ webpack-libs-optimizations

比如:moment.js 删除未使用的地区、react-router 移除未使用的模块,生产环境去除 react propTypes 声明等。

6、对于ES6模块开启模块连接

也叫做作用域提升(Scope Hoisting)

早期的时候,为了隔离 CommonJS/AMD 模块,webpack 在打包的时候,会把每个模块都打包到一个函数中,这样就会增大每个模块的大小和性能开销。webpack 2 的时候支持了 ES 模块,然后 webpack 3 的时候使模块连接成为了可能。

【原理】:它会分析模块间的依赖关系,尽可能将被打散的模块合并到一个函数中,但不能造成代码冗余,所以只有被引用一次的模块才能被合并。由于需要分析模块间的依赖关系,所以源码必须是采用了ES6模块化的,否则Webpack会降级处理不采用Scope Hoisting。

开启模块连接之后,打出的包将会具有更少的模块,以及更少的模块开销。如果在生产模式下使用 webpack 4,则模块连接已经启用。

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

★ 注意:为什么默认情况下不启用此行为?连接模块很酷,但是它增加了构建时间,并中断了热模块替换。这就是为什么应该只在生产中启用它。

三十分钟掌握Webpack性能优化

7、如果觉得有意义的话,使用 externals

具体请参考:webpack-configuration-externals

二、使用长期缓存

1、文件名输出

缓存包(bundle),并通过更改包名称(bundle name)来区分版本,将文件名替换成 [name].[chunkname].js

[hash] 替换:可以用于在文件名中包含一个构建相关(build-specific)的 hash;
[chunkhash] 替换:在文件名中包含一个 chunk 相关(chunk-specific)的哈希,比[hash]替换更好;
[contenthash] 替换:会根据资源的内容添加一个唯一的 hash,当资源内容不变时,[contenthash] 就不会变。

const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
-   entry: './index.js',
+   entry: {
+     main: './index.js',
+   },
    output: {
-     filename: 'bundle.js',
+     filename: '[name].[contenthash].js',  // / → bundle.8e0d62a03.js
      path: path.resolve(__dirname, 'dist')
    }
    plugins: [
      new HtmlWebpackPlugin({
-       title: 'Output Management'
+       title: 'Caching'
      })
    ],
  };

Hash vs chunkhash vs ContentHash

2、提取第三方库和样板代码

bundle 拆分成程序代码(app)、第三方库代码(vendor)和运行时代码(runtime)。

  • 开启智能 code splitting

在 webpack 4 中添加以下的代码,当第三方库代码大于 30 kb 时(未压缩和未gzip前),webpack 能够自动提取 vendor 代码,并且如果你在路由层面使用了代码分割的话,它也能够提取公共代码。

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
    }
  },
};

这样,每次打包都会生成两个文件:main.[chunkhash].jsvendors~main.[chunkhash].js (for webpack 4). 在 webpack 4 中, 当第三方库依赖很小的时候,vendor 包可能不会被生成,但也没关系。

  • webpack 运行时代码

Webpack 在入口 chunk 中,包含了其运行时的引导代码: runtime,以及伴随的 manifest 数据,runtime 是用来管理模块交互的一小片段代码。当你将代码分割成多个文件时,这段代码包含了 chunk id 和模块文件之间的映射,包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。

Webpack 会将这个运行时包含到最后生成的 chunk 中,即 vendor。每次有任何块发生变化时,这段代码也会发生变化,导致 vendor bundle 发生变化。

【解决方法】:设置 runtimeChunktrue 来为所有 chunks 创建一个单一的运行时包:

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

webpack 运行时代码很小,内联它,可以减少 HTTP 请求。

// webpack.config.js
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(),
  ],
};

webpack-concepts-manifest

3、代码懒加载

单页应用中,使用 import 对不关键的代码进行懒加载。

// 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 看到 import('./module.js') 时,它会自动把该模块从 chunk 中移除,只有在执行的时候才会被下载。

这会使 main 模块更小,能够减少初始加载时间,并且也能很好的提高缓存,如果你在 main chunk 中改了代码,懒加载的模块不会被影响。

按路由/页面分割代码(Code Splitting),以避免加载不必要的内容。

单页应用中,除了通过 import() 进行懒加载,还可以通过框架层面的手段来进行。
React 应用懒加载——> Code Splitting(react-router) 或者 React.lazy(react doc)

WebpackGuides-Caching
WebpackConcepts-The Manifest

4、模块标识符

使模块标识符更稳定

在 webpack 构建时,每个 module.id 会基于默认的解析顺序(resolve order)进行增量,也就是说,当解析顺序发生变化,ID 也会随之改变。如:当新增一个模块的时候,它可能会出现在模块列表的中间,那么它之后的模块 ID 就会发生变化。

如果在业务代码里新引入一个模块,则:

  • main bundle 会随着自身的新增内容的修改,而发生变化 ——> 符合预期
  • vendor bundle 会随着自身的 module.id 的修改,而发生变化 ——> 【不符合预期】
  • runtime bundle 会因为当前包含一个新模块的引用,而发生变化 ——> 符合预期
+ const webpack = require('webpack');

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

为了解决这个问题,模块 ID 通过 HashedModuleIdsPlugin 来进行计算,它会把基于数字增量的 ID 替换成模块自身的 hash。这样的话,一个模块的 ID 只会在重命名或者移除的时候才会改变,新模块不会影响到它的 ID 变化。

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

三、监控和分析应用程序

在开发阶段使用 webpack-dashboardbundlesize 来调整应用程序的大小

  • webpack-dashboard

webpack-dashboard 通过展示依赖项大小、进度和其他细节来增强 webpack 输出,有助于跟踪大型依赖项。

npm install webpack-dashboard --save-dev
// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');

module.exports = {
  plugins: [
    new DashboardPlugin(),
  ],
};
  • bundlesize

bundlesize 用于验证 webpack 的资源不超过指定的大小,当应用程序变得太大时能够及时得知。

(1)运行打包命令
(2)开启 bundlesize

npm install bundlesize --save-dev

(3)在 package.json 中指定文件大小限制

// package.json
{
  "bundlesize": [
    {
      "path": "./dist/*.png",
      "maxSize": "16 kB",
    },
    {
      "path": "./dist/main.*.js",
      "maxSize": "20 kB",
    },
    {
      "path": "./dist/vendor.*.js",
      "maxSize": "35 kB",
    }
  ]
}

(4)执行 bundlesize

npx bundlesize

或者用 npm 执行:

// package.json
{
  "scripts": {
    "check-size": "bundlesize"
  }
}

通过 webpack-bundle-analyzer 分析包的大小

webpack-bundle-analyzer 能够扫描 bundle 并对其内部内容进行可视化呈现,从而可以发现大型的或者不必要的依赖项。

npm install webpack-bundle-analyzer --save-dev
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin(),
  ],
};

运行生产构建,该插件会在浏览器中打开可视化页面。

默认情况下,统计页面显示的是已解析文件的大小(当文件出现在包中时)。您可能想比较 gzip 之后的大小,因为它更接近实际用户体验,可以使用左边的边栏来切换大小。

对于报告,我们需要关注的点有:

  • 大型依赖项:为什么这么大?是否有更小的替代方案(例如,用 Preact 代替 React)?您是否使用了该库包含的所有代码(例如,Moment.js 包含了许多 经常不使用且可能被删除的地区设置)?

  • 重复的依赖关系:您是否看到同一个库在多个文件中重复出现?(在 webpack 4 中使用 optimization.splitChunks.chunks 将重复的依赖关系移动到一个公共文件)。或者某个包具有相同库的多个版本?

  • 相似的依赖关系:是否有类似的库可以做大致相同的工作?(例如,momentdate-fns,或 lodashlodash-es),试着只用一个工具。

四、总结

(1)削减不必要的字节。压缩所有内容,删除未使用的代码,明智地添加依赖项;
(2)按路由拆分代码。只加载现在真正需要的东西,稍后再加载其他东西;
(3)缓存代码。应用程序的某些部分(如第三方库)更新的频率低于其他部分,将这些部分分离到文件中,以便只在必要时重新下载;
(4)追踪代码大小。使用像 webpack-dashboard 和 webpack-bundle-analyzer 这样的工具来了解你的应用程序有多大。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342