webpack性能优化

性能分析

1. 统计基本信息

使用webpack内置的stats

可以统计出构建时间、构建资源清单及资源大小等信息

使用方法:

1. cli

webpack --env production --json > stats.json

2. node API

webpack(config, (err, stats) => {
  console.log(stats);
});

2. 速度分析

使用speek-measure-webpack-plugin

插件功能

  1. 分析出整个构建时间和每个loader和plugin的构建时间
  2. 时间过长的标红,较长的标黄

插件使用:包裹webpack的配置

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

const webpackConfig = smp({
  // webpack配置
});

3. 体积分析

使用webpack-bundle-analyzer

以可视化形式展示打包依赖模块的体积。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

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

构建完成后会启动本地服务,serve 8888端口,浏览器中访问就能看到分析结果。

提升构建速度

使用高版本的webpack和nodejs

高版本的webpack和nodejs构建速度更快

webpack4优化原因:

  • v8带来的优化(for of 代替forEach,Map、Set代替Object、includes代替indexOf)
  • 默认使用更快的md4 hash算法
  • webpack AST可以直接从loader传递给AST,减少解析时间
  • 使用字符串方法代替正则

多进程多实例构建

资源并行解析可选方案

  • HappyPack
    作者已经不维护,建议使用webpack官方提供的"thread-loader"
  • thread-loader
{
  test: /.js$/,
    user: [
      {
        loader: 'thread-loader',
        options: {
          workers: 3
        }
      },
      'babel-loader'
    ]
}
  • parallel-webpack

多进程多实例并行压缩

1. 方法一,使用parallel-uglify-plugin插件

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      // ...
    })
  ]
};

2. 方法二,使用uglifyjf-webpack-plugin

目前webpack官方推荐使用terser-webpack-plugin

3. 方法三,使用terser-webpack-plugin,开启parallel参数

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: 4
      })
    ]
  }
};

进一步分包:预编译资源模块

1. 使用html-webpack-externals-plugin

将react、react-dom基础包通过cdn引入,不打入bundle中

使用方法:

const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

plugins: [
  new HtmlWebpackExternalsPlugin({
    externals: [
      {
        module: 'react',
        entry: '//path/to/your/cdn-domain/react.min.js',
        global: 'React'
      },
      ...
    ]
      });
]

效果

<script type="text/javascript" src="//path/to/your/cdn-domain/react.min.js"></script>

2. 预编译资源模块,使用DLLPlugin和DllReferencePlugin

通常来说,我们的代码都可以至少简单区分成业务代码第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。

使用dll时,构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做webpack.config.jswebpack.dll.config.js

步骤:

  1. 使用DLLPlugin进行分包,对第三方包打包,完成后打包结果保存在项目中,后面就不需要再构建第三方包了。
  2. 每次构建业务项目时候,使用DllReferencePlugin实现对构建好的第三方包dll的解析和处理。

示例:

使用DLLPlugin进行分包

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

module.exports = {
  context: process.cwd(),
  entry: {
    library: [
      'react',
      'react-dom',
      'redux'
    ]
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, './build/library'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: './build/library/[name].json'
    })
  ]
};

使用DllReferencePlugin对manifest.json引用

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./build/library/manifest.json')
    })
  ]
};

引用效果

<script src="/build/library/library.dll.js"></script>

原理

使用DLLPlugin对第三方库打包时候,会生成打包结果和manifest.json文件到指定目录,文件中包含各第三方包的引用关系等信息。

使用DllReferencePlugin插件打包业务代码时候,我们通过配置告诉插件DLLPlugin打包的产物的目录,DllReferencePlugin会分析manifest文件,DLL的包不会参与打包构建过程,并且还生成相关的引用。

3. 使用externals选项

使用externals选项可以排除指定的第三方模块,在构建过程中忽略它们。

使用方法示例:

首先在html中引入第三方模块

<script src="https://example-cdn.com/react.min.js"></script>

然后在externals选项里面配置要排除的模块和引用方式

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

在项目中引用

import React from 'react';

充分利用缓存提升二次构建速度

缓存思路

  1. babel-loader开启缓存
  2. terser-webpack-plugin开启缓存
  3. 使用cache-loader或者hard-source-webpack-plugin

缩小构建目标

目的:尽可能少构建模块

比如babel-loader不解析node_modules

module.exports = {
  rules: {
    test: /.js$/,
    loader: 'happypack-loader',
    exclude: 'node_moudles'
  }
};

减少文件搜索范围

  1. 优化resolve.modules配置(减少模块搜索层级)
  2. 优化resolve.mainFields配置(缩小模块入口搜索范围)
  3. 优化resolve.extensions配置(比如限定.js,其他引用时候补全后缀)
  4. 合理使用alias(缩小模块引用路径搜索范围)

使用oneOf

通常来讲,同一种类型的文件只能由一个loader处理,那么正常来讲的逻辑应该是,比如我是一个css文件,那么我匹配到test为css后缀的loader我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。

而Oneof语法就是解决这个问题的,使文件一旦匹配上loader之后就立即解析,省去了全盘遍历这个不必要的过程。

如果对于需要多个loader共同解决的文件类型,比如js。那就需要把其中的loader放历Oneof之外,这样才能实现loader也能同时执行到。

提升加载和运行速度

使用Tree Shaking擦除无用的js和css

摇树js

Tree-Shaking原理

Tree-shaking的本质是消除没有用到的代码。主要的效果是,引用了但没有使用的模块,不会被打包到最终的bundle中。

Tree-shaking要求模块是ESM,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不运行代码,从语法上对代码进行分析,ES6之前的模块化规范,比如我们可以动态require一个模块,只有执行后才知道是否要引用某个模块,引用的是什么模块(因为require是运行时调用,所以require理论上可以运用在代码的任何地方,而且require支持传入表达式作为参数,只能在运行时才知道引入的是哪个模块),这个就不能通过静态分析去做优化。

webpack默认支持Tree-shaking,如果mode为"production"webpack在构建会做Tree-shaking的操作。

摇树css

摇树css的基本思路是,给定content和css,分析content中用到的选择器,然后分析css文件中没有用到的选择器,将其移除。

摇树css工具有:

  1. PurifyCSS
    使用purgecss-webpack-plugin
  2. uncss

scope-hoisting

Scope Hoisting使用和原理分析

webpack 的 scope hoisting 是什么?

scope hoisting 是 webpack3 的新功能,直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件“提升到”它的引入者顶部。

在之前版本中,webpack打包会把每个模块用闭包封装,通过webpack_require 引用。

这样会存在问题:

  1. ⼤量作用域包裹代码,导致体积增大(模块越多越明显)
  2. 运行代码时创建的函数作⽤域变多(每个模块引用都要创建一个函数作用域),内存开销变大

为了解决这两个问题,webpack启用scope hoisting,将每个模块都提升到引入者顶部,这样模块不会因为依赖链路较深而导致调用栈变深,它们都在同一层。

这样就解决了上面的两个问题:

  1. 代码量明显减少。
  2. 调用栈变浅,减少了创建作用域的内存和计算损耗,提升了运行速度。

使用webpack进行图片压缩

图片压缩实际是使用了基于node库的imagemin或者tinypng API

在webpack中配置image-webpack-loader,这个loader实际是使用了imagemin进行图片压缩

优化polyfill方案

polyfill的方案:

1. babel-polyfill

将babel-polyfill作为一个单独的入口打包
这样做的一个问题是会将所有polyfill代码都打包进去(200k左右),导致代码体积过大

const path = require('path');

module.exports = {
  entry: [
    'babel-polyfill',
    path.resolve(__dirname, './src/index.js')
  ]
};

2. babel-presets的选项中“useBuiltIns”

选项值为“false”时候,不加入polyfill。
选项值为“entry”时候,将所有polyfill打包进项目。
选项值为“usage”时候,按需加载,并且做了优化:将polyfill的工具方法提取成公共资源,而不会每个 polyfill代码都内联相同的工具方法 。
此外babel-presets中还支持根据支持的浏览器来选择polyfill,这通过"target"属性配置。

// .babelrc
{
  "presets": [
    "@babel/preset-env",
    {
      "useBuildIns": "useage"
    }
  ]
}

3. @babel/runtime和@babel/plugin-transform-runtime

@babel/runtime实现polyfill的功能,它分析代码,然后添加相关的polyfill,即实现了polyfill的按需加载。它和上述两种方法的区别是,它在添加polyfill代码时候,不会污染全局变量,而是定义局部方法来实现polyfill。因为这个特点,它更适合用在第三方库中,而上面两种适合用在业务代码项目中
其缺点在于不支持实例方法的polyfill,如arr.includes(1);
由于 @babel/runtime也是使用内联代码实现polyfill,因此可能多个文件中会内联相同的工具方法。@babel/plugin-transform-runtime用来解决这个问题,它提取公共的工具方法,每个文件使用时候引入相关的工具方法,这样减少的代码体积。

4. polyfill-service,使用动态polyfill服务

根据浏览器userAgent选择相应的polyfill,有些浏览器支持的,就不再下发冗余polyfill。
可以使用官方的服务。
或者自建polyfill服务。
可能存在的问题:浏览器ua不准,有些国内浏览器修改ua导致polyfill判断错误,降级方案是下载所有polyfill

https://cdn.polyfill.io/v2/polyfill.min.js

使用prerender-spa-plugin预渲染

prerender-spa-plugin

prerender-spa-plugin插件启用无头浏览器,加载项目的路由,并渲染出首屏页面(也可以配置其他路由),然后生成静态页面,保存在指定的目录。

我们的静态资源服务器就可以serve预渲染的页面了。

使用这个插件相当于在构建阶段就渲染好了首屏页面,极大地提升了首屏性能。

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

推荐阅读更多精彩内容