webpack性能优化之秒开

1,261 阅读9分钟

前言

作为前端开发人员,大家众所周知的是一个页面打开的速度对于用户体验和用户流失率来说至关重要,甚至可以说秒开是一个web项目的基本要求。接下来,我将分享一下如何用webpack神器进行web项目的性能优化。性能优化分为两部分,第一个部分是web打开时性能优化,另一个则是webpack构建速度优化。 以下优化方式基于webpack 4.16.0

一.秒开优化

对于秒开优化,核心还是在于尽可能地在首页打开时只加载首页必须用到的东西。减少请求,减少体积。对于后续用到的东西,可以在浏览器空闲的时候提前下载。

1.压缩js文件
npm install terser-webpack-plugin --save-dev

optimization: {
		concatenateModules:true,
    minimizer: [
     new TerserPlugin({
     terserOptions: {
          output: {
          	comments:false
          },
          extractComments:false
        }
     })
    ]
 }
2.压缩css

将css通过外联的方式引入,提高缓存使用效率

npm install --save-dev optimize-css-assets-webpack-plugin
npm install --save-dev mini-css-extract-plugin

 module.exports = {
 optimization: {
    minimizer: [
     new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.optimize\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    })
    ]
 },
  plugins: [
    new MiniCssExtractPlugin({
       chunkFilename: 'assets/styles/[id]-[contenthash:8].css',
       ignorOrder:true
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          'css-loader',
        ],
      },
    ],
  },
};
3.内联图片、文件命名
npm install --save-dev file-loader
npm install --save-dev url-loader

rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
            name:'assets/images/[name]-[contenthash:8].[ext]'
            }
          },
          {
            loader: 'url-loader',
            options: {
              limit: 10240
            }
          }
        ]
      }
    ]
    
4.字体文件处理
rules: [
      {
        test: /\.(ttf|woff)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
            	name:'assets/styles/font/[name]-[contenthash:8].[ext]'
            }
          },
          {
            loader: 'url-loader',
            options: {
              limit: 10240
            }
          }
        ]
      }
    ]
    
5.svg处理

这个其实跟性能关系不大,只是为了方便使用svg而已

icons 下面的 index.js 写入以下内容:

//全局注册
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg组件
// register globally
Vue.component('svg-icon', SvgIcon)

//引入svg,这里最好是不同模块引入各自的文件夹,避免svg体量过大
const requireAll = requireContext => requireContext.keys().map(requireContext)
const req = require.context('./svg', false, /\.svg$/)
requireAll(req)

入口 main.js 将 index.js 引入:
import '@/icons'
使用:
<svg-icon icon-class="eye"></svg-icon>

webpack 配置:
npm install svg-sprite-loader -D
npm install svgo svgo-loader --save-dev
{
  test: /\.svg$/,
  loader: [{
    	loader:'svg-sprite-loader',
      option: {
        symbolId:"icon-[name]"
      }
  	},
  	{
    	loader:'svgo-loader',
      options: {
              plugins: [
                {removeTitle: true},
                {convertColors: {shorthex: false}},
                {convertPathData: false}
              ]
              }
  	},
  ]
}
6.分析构建包

因为各个项目写的代码不同,引入的第三方库也不同,所以想要看清楚项目打包后到底要加载哪些文件,打出了哪些bundle,还需要进一步分析。这里需要使用webpack-bundle-analyzer。需要注意的是,这个插件只在分析的时候用,平日构建不要用,因为会拖累构建速度。这个步骤要注意分析里面比较大的包,如超过一兆的包看分包是否合理:1.在同一个地方用到的库打到一起是合理的 2.只加载自己当前页面用到的库

npm install --save-dev webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
7.合理分包(重要)

默认情况下,webpack会给你提供默认的分包配置,其中会将你的node_module依赖全部达到vender这个包中,这种方式在小项目中简单且体积小。但问题是是在大型项目中依赖的第三方库很多,而且首次打开页面时往往只需要用到极少部分第三方库,甚至登录页不用第三方库。所以我们需要根据项目实际情况合理分包。分包依据为:1.超过1兆的包拿出来单独分包 2.首次打开需要的包拿出来 3.项目中依赖频率高的拿出来 4.BundleAnalyzerPlugin分析后发现不合理的包拿出来

optimization: {
    splitChunk:{
    	chunks:"all",
      minSize: 30000,  //表示在压缩前的最小模块大小,默认值是30kb
    	minChunks: 1,  // 表示被引用次数,默认为1;
      maxAsyncRequests: 5,  //所有异步请求不得超过5个
      maxInitialRequests: 3,  //初始话并行请求不得超过3个
     	automaticNameDelimiter:'~',//名称分隔符,默认是~
      name: true,  //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔
      cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk
      // 举例,echarts是登陆后才需要引入的,单独拿出来
      	echarts:{
      		minChunks:1,
          test:/[\\/]node_modules[\\/](echarts|echart-gl)/,
          priority:10,
          name:'echarts'
      	},
      	 // 举例,demoCommonModule是被很多其他模块调用的,单独拿出来,不然会被分散重复打到其他的包去
      	demoCommonModule:{
      		minChunks:1,
          test:/[\\/]views[\\/]demoCommonModule/,
          priority:10,
          name:'echarts'
      	}
      	
      	... 其他模块也采用类似的方式抽出来,具体要看BundleAnalyzerPlugin分析是否合理
      }
 }
8.延迟加载(重要)

很多人对于公共引入的内容直接就放在app.js里面了,这样导致web项目首次启动的时候会去下载这一大堆文件,导致页面加载慢。可以考虑分出优先级,将不是必须的公共引入延后。

Before:

index.js

import vue from "vue";
import 'echarts' from "echarts";
import "@/icons"
import elementUI ...
import ...
import ...
import ...

After:

index.js

//只引入首次进来必须的,我这里首次进来是登录页,所以只需要引入少量的库
import vue from "vue";
import {button,from,...} from "elementUI";

//新建一个全局引入js,globalImport.js
import vue from "vue";
import 'echarts' from "echarts";
import "@/icons"
import elementUI ...
import ...
import ...
import ...
export default {}

//可以在路由守卫,也可以在你的全局非首次页面的layout里面引入。注意使用预加载,这样可以在浏览器空闲的时候把公共包下载下载待使用
let globalImportPromise = null

if(to.name !== "login"){
	if(globalImportPromise){
		return next();
	}else{
		globalImportPromise = import(/* webpackChunkName:"global" */ /* webpackPreFetch:true*/ "@/utils/globalImport.js");
		globalImportPromise.then(_=>next());
	}


}
9.固化bundle名字(重要)

webpack在打包过程中会自动给你的bundle按照顺序递增命名,这样的问题在于当你只改了其中一个文件也会导致全部文件的编号被改变,从而使得缓存全部失效。所以这里我们需要将文件名字固化,只有改了的那个文件编号才会需要变更。(这个可以详看参考链接2)

 runtimeChunk: true;

//runtime 内容内置到html中
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

// 注意一定要在HtmlWebpackPlugin之后引用
// inline 的name 和你 runtimeChunk 的 name保持一致
new ScriptExtHtmlWebpackPlugin({
  //`runtime` must same as runtimeChunk name. default is `runtime`
  inline: /runtime\..*\.js$/
});


const seen = new Set();
const nameLength = 4;

new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  const modules = Array.from(chunk.modulesIterable);
  if (modules.length > 1) {
    const hash = require("hash-sum");
    const joinedHash = hash(modules.map(m => m.id).join("_"));
    let len = nameLength;
    while (seen.has(joinedHash.substr(0, len))) len++;
    seen.add(joinedHash.substr(0, len));
    return `chunk-${joinedHash.substr(0, len)}`;
  } else {
    return modules[0].id;
  }
});
10.使用HTTP2.0

目前基本上各大浏览器都已经支持http2.0,http2.0在1.1的基础上做了许多优化,例如:

  • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
  • 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。多路复用
  • header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
  • 服务端推送(server push),同SPDY一样,HTTP2.0也具有server push功能。目前,有大多数网站已经启用HTTP2.0,例如YouTuBe,淘宝网等网站,利用chrome控制台可以查看是否启用H2。
11.使用gzip压缩
npm install compression-webpack-plugin --save-dev

const CompressionPlugin = require('compression-webpack-plugin');
 
module.exports = {
  plugins: [new CompressionPlugin()],
};
12.域名预解析

域名预解析是让浏览器在一开始拿到dom的时候在开头就去查找域名的dns,减少后续等待查询的时间

<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
<link rel="dns-prefetch" href="http://nsclick.baidu.com" />
13.使用cdn

对于第三方库,也可使用cdn的方式引入,从而提高下载速度(如果使用了缓存,那这条非必选。因为cdn属于不同域名,二次打开的时候有时候cdn的缓存会失效,导致还不如不用cdn)

npm install --save-dev html-webpack-externals-plugin

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

plugins:[
//这个插件的好处是自动帮你加上externals避免webpack打包进去,并在你的html内添加标签
new HtmlWebpackExternalsPlugin({
  externals: [
    {
      module: 'jquery',
      entry: 'https://unpkg.com/jquery@3.2.1/dist/jquery.min.js',
      global: 'jQuery',
    },
     {
      module: 'moment',
      entry: 'https://cdn.bootcdn.net/ajax/libs/moment.js/2.27.0/moment.min.js',
      global: 'moment',
      attributes:{
      	defer:'defer'
      }
    },
  ],
})
]
14.构建成离线应用

离线应用是指使用web worker 将文件全部下载到本地浏览器,等到再次打开页面时可以极快速地打开页面。(如果使用了强缓存,则离线应用可以不用,因为他们作用是差不多的)

npm install offline-plugin -D

入口文件中添加
require('offline-plugin/runtime').install()

var OfflinePlugin = require('offline-plugin');
 
module.exports = {
  plugins: [
    new OfflinePlugin()
  ]
}
15.代码优化

es6写的模块中,如果包含多个工具方法,最好不要使用export {}的方式导出,这样会导致webpack treeshaking 无法将不需要的方法清除掉,需要使用export method1;export method2 的方式导出

//不推荐
export default {
	method1:method1,
	method2:method2,
	method2:method2,
}

//推荐:
export method1;
export method2;
export method3;

二.构建优化

1.使用缓存
  • loader 缓存

    loader: 'babel-loader?cacheDirectory=ture'
    
  • 插件缓存

    其实更推荐使用loader本身的缓存,因为插件的缓存比较容易失效,且插件的执行也比较耗时

    npm install --save-dev hard-source-webpack-plugin 
    
    var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
     
    module.exports = {
      plugins: [
        new HardSourceWebpackPlugin()
      ]
    }
    
2.多线程

多线程打包,提高执行的速度。(这个不是万能的,如果你的电脑配置差的话开了多线程反而会拖垮执行速度)

npm install --save-dev thread-loader
用法:
把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行

const threadLoader = require('thread-loader');
//线程预热
threadLoader.warmup({}, [
  'babel-loader',
  'babel-preset-es2015',
  'sass-loader',
]);

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          "expensive-loader"
        ]
      }
    ]
  }
}

3.dll

将一些公共包打成dll包,这样下次构建的时候webpack就会直接略过这些包

4.extranals

第三方类库的包可以用script标签的方式引入,这样webpack打包的时候也会略过这些包

总结:

对于web项目的构建,webpack只是一个可以帮我们节省力气的工具,就算你用gulp也是一样可以做到的。秒开或提高打开的速度核心仍在是在减少体积、只加载必要的库、尽可能早地获取资源这三个环节,于是就有了压缩、分包、预加载等机制。同时整个前端领域的技术也是在不对地发展,所以我们要善于利用技术进步的优势,使用http2.0、gzip、缓存、离线应用、域名预解析等功能。优化是个永无止境的工程,越像往前就要做到更加的细化,同时应该注重优化空间与业务价值之间的效益比,一般上来说,0.3秒跟0.5秒对于用户而言是差别不大的,这时候重点可以不是优化速度,而是其他方面能提高业务价值的地方了。

参考链接:

1.webpack 官网

2.手摸手,带你用合理的姿势使用webpack4(下)