webpack性能优化

385 阅读3分钟

为什么要进行性能优化?

webpack是我们重要的代码打包及构建工具,因此它的性能优化也影响到我们的代码质量

所以webpack的性能优化还是很有必要的

可以从以下几点来说:

  • 优化开发体验
    • 提升效率
    • 优化构建速度
    • 优化使⽤体验
  • 优化输出质量
    • 优化要发布到线上的代码,减少⽤户能感知到的加载时间
    • 提升代码性能,性能好,执⾏就快

缩小搜索Loader的文件范围

优化loader配置

  • test include exclude三个配置项来缩⼩loader的处理范围
  • 推荐include
//string
include: path.resolve(__dirname, "./src"),
//array
include: [
     path.resolve(__dirname, 'app/styles'),
     path.resolve(__dirname, 'vendor/styles')
 ]

优化resolve.alias配置

resolve.alias配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径

类似于起别名

 resolve: {
        alias: {
            // 起别名
            "@img": path.resolve(__dirname, "./src/images")
        },
}

//index.less
body {
  #app {
    height: 400px;
    background: url("@img/logo.png") 0 0 no-repeat;
  }
}
//直接使用@img

优化resolve.extensions配置

resolve.extensions在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存 在。

默认值:

 // 导入模块后缀列表
 // 推荐所有的模块都加上后缀
 extensions: [".js", ".json"]
  • 后缀尝试列表尽量的小。
  • 导⼊语句尽量的带上后缀。

优化resolve.modules配置

resolve.modules会告诉 webpack 解析模块时应该搜索的目录

寻找第三⽅模块,默认是在当前项⽬⽬录下的node_modules⾥⾯去找,

如果没有找到,就会去上⼀级 ⽬录../node_modules找,再没有会去../../node_modules中找,以此类推,和Node.js的模块寻找机制 很类似。 如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。

module.exports={
 resolve:{
 modules: [path.resolve(__dirname, "./node_modules")]
 }
}

使用externals优化cdn静态资源

三种情况下可使用:

  • 公司有cdn
  • 静态资源有部署到cdn 有链接了
  • 我想使⽤cdn!!!!!!!!

使用以后我的bundle⽂件⾥,就不⽤打包进去这个依赖了,体积会⼩

我们可以将⼀些JS⽂件存储在 CDN 上(减少 Webpack 打包出来的 js 体积),在 index.html 中通过 标签引⼊,

如:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Document</title>
</head>
<body>
 <div id="root">root</div>
 <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>

我们希望在使⽤时,仍然可以通过 import 的⽅式去引⽤(如 import $ from 'jquery' ),并且希望 webpack 不会对其进⾏打包,此时就可以配置 externals

//webpack.config.js
module.exports = {
 //...
 externals: {
 //jquery通过script引⼊之后,全局中即有了 jQuery 变量
 'jquery': 'jQuery'
 }
}

使用静态资源路径publicPath(CDN)

CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把⽹ ⻚的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL。

//webpack.config.js
output:{
 publicPath: '//cdnURL.com', //指定存放JS⽂件的CDN地址
}
注意要点:
  • 咱们公司得有cdn服务器地址
  • 确保静态资源⽂件的上传与否

CSS文件的处理

  • 使用less或者sass当做css技术栈

    npm install less less-loader --save-dev
    
    //webpack.config.js--lodaer
    {
     test: /\.less$/,
     use: ["style-loader", "css-loader", "less-loadr"]
    }
    
  • 使用postcss为样式自动补齐浏览器前缀

    npm i postcss-loader autoprefixer -D //安装对应依赖
    
    //新建postcss.config.js
    module.exports = {
         plugins: [
             require("autoprefixer")({
             	overrideBrowserslist: ["last 2 versions", ">1%"]
         	})
     	]
    };
    //index.less
    body {
     div {
         display: flex;
         border: 1px red solid;
     }
    }
    //webpack.config.js
    {
     	test: /\.less$/,
     	include: path.resolve(__dirname, "./src"),
         use: [
         "style-loader",
         "css-loader",
         "less-loader",
         "postcss-loader"
        ]
    },
    

    提醒:postcss 使用一定要创建postcss.config.js进行配置

  • 借助MiniCssExtractPlugin 完成抽离css

    如果不做抽取配置,我们的 css 是直接打包进 js ⾥⾯的,我们希望能单独⽣成 css ⽂件。 因为单独⽣ 成css,css可以和js并⾏下载,提⾼⻚⾯加载效率

    npm install mini-css-extract-plugin -D //安装mini-css-extract-plugin插件
    

    使用:

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
     rules: [
                {
                    test: /\.less$/,
                    include: path.resolve(__dirname, "./src/css"),
                    use: [
                    // "style-loader", 
                    // 不再需要style-loader,⽤MiniCssExtractPlugin.loader代替
                        {
                            loader: miniCssExtractPlugin.loader,
                            options: {
                                publicPath: "../",
                            },
                        },
                        "css-loader",
                        "postcss-loader",
                        "less-loader",
                    ]
                },
       ]
    plugins: [
         new MiniCssExtractPlugin({
             filename: "css/[name]_[contenthash:6].css",
             chunkFilename: "[id].css"
         })
     ]
    

CSS文件的压缩

  • 借助 optimize-css-assets-webpack-plugin
  • 借助cssnano

安装:

npm install cssnano -D
npm i optimize-css-assets-webpack-plugin -D

使用:

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
new OptimizeCSSAssetsPlugin({
 cssProcessor: require("cssnano"), //引⼊cssnano配置压缩选项
 cssProcessorOptions: {
 	discardComments: { removeAll: true }
 }
})

压缩HTML

借助html-webpack-plugin

使用:

new htmlWebpackPlugin({
 template: "./index.html",
 filename: "index.html",
 minify: {
     // 压缩HTML⽂件
     removeComments: true, // 移除HTML中的注释
     collapseWhitespace: true, // 删除空⽩符与换⾏符
     minifyCSS: true // 压缩内联css
  }
 }),

图片优化

在 Webpack 中可以借助img-webpack-loader来对使⽤到的图⽚进⾏优化。它⽀持 JPG、PNG、GIF 和 SVG 格式的图⽚,因此我们在碰到所有这些类型的图⽚都会使⽤它。

安装:

npm install image-webpack-loader --save-dev

macos系统版本需要依赖libpng,需要翻墙!!!!!

使用:

 rules: [{
            test: /\.(gif|png|jpe?g|svg)$/i,
            use: [
                'file-loader',
                {
                    loader: 'image-webpack-loader',
                    options: {
                        mozjpeg: {
                            progressive: true,
                            quality: 65
                        },
                        // optipng.enabled: false will disable optipng
                        optipng: {
                            enabled: false,
                        },
                        pngquant: {
                            quality: [0.65, 0.90],
                            speed: 4
                        },
                        gifsicle: {
                            interlaced: false,
                        },
                        // the webp option will enable WEBP
                        webp: {
                            quality: 75
                        }
                    }
                },
            ],
        }],

development vs Production模式区分打包

我们需要安装一个依赖

npm install webpack-merge -D

使用案例:

const merge = require("webpack-merge")
const commonConfig = require("./webpack.common.js")
const devConfig = {
 //这里写对应模式的配置
}
module.exports = merge(commonConfig,devConfig)
//package.js
"scripts":{
	"dev":"webpack-dev-server --config ./webpack.dev.js",
    "build": "webpack --config ./webpack.pro.js"
}

基于环境变量区分

  • 借助cross-env

    npm i cross-env -D
    

    package⾥⾯配置命令脚本,传⼊参数

//package.json
"test": "cross-env NODE_ENV=test webpack --config ./webpack.config.test.js",

​ 在webpack.config.js⾥拿到参数

process.env.NODE_ENV
//外部传⼊的全局变量
module.exports = (env)=>{
 if(env && env.production){
 return merge(commonConfig,prodConfig)
 }else{
 return merge(commonConfig,devConfig)
 }
}

//外部传⼊变量
scripts:" --env.production"

tree Shaking

webpack2.x开始⽀持 tree shaking 概念,顾名思义,"摇树",

摇树 - 大力的晃动树木 => 会把树上的枯叶都摇下来

清除无用的css ,无用的js 也就是 Dead-code

Dead Code ⼀般具有以下⼏个特征

  • 代码不会被执⾏,不可到达
  • 代码执⾏的结果不会被⽤到 代码只会影响死变量(只写不读)
  • Js tree shaking只⽀持ES module的引⼊⽅式!!!!(import || module.exports)

Css tree shaking

首先我们要安装依赖

npm i glob-all purify-css purifycss-webpack --save-dev // 安装

使用:

const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
 	// 清除⽆⽤ css
     new PurifyCSS({
         paths: glob.sync([
         // 要做 CSS Tree Shaking 的路径⽂件
         // 请注意,我们同样需要对 html ⽂件进⾏ tree shaking
         path.resolve(__dirname, './src/*.html'), 
         path.resolve(__dirname, './src/*.js')
     ])
 })
]

注意一点就是Css tree shaking 只能处理使用mini-css-extract-plugin插件抽离过的css

JS tree shaking

只⽀持import⽅式引⼊,不⽀持commonjs的⽅式引⼊

案例:

//expo.js
export const add = (a, b) => {
  return a + b;
};
export const minus = (a, b) => {
  return a - b;
};

//index.js
import { add } from "./expo";
add(1, 2);

//webpack.config.js
optimization: {
 	usedExports: true // 哪些导出的模块被使⽤了,再做打包
}

只要mode是production就会⽣效,develpoment的tree shaking是不⽣效的,因为webpack为了 ⽅便你的调试

可以查看打包后的代码注释以辨别是否⽣效。

⽣产模式不需要配置,默认开启

sideEffects 处理副作用

因为我们不知道某些导入是否为有用,有可能只是还没有引用

sideEffects字段,默认为true,即默认这个npm包有副作用,就可以把一些import语句保留下来

可以设置为false,但是需要配合usedExports。

//package.json
"sideEffects":false //正常对所有模块进⾏tree shaking , 仅⽣产模式有效,需要配合usedExports

 //或者 在数组⾥⾯排除不需要tree shaking的模块
"sideEffects":['*.css','@babel/polyfill']

代码分割 code Splitting

单⻚⾯应⽤spa:

打包完后,所有⻚⾯只⽣成了⼀个bundle.js

  • 代码体积变⼤,不利于下载
  • 没有合理利⽤浏览器资源

多⻚⾯应⽤mpa:

如果多个⻚⾯引⼊了⼀些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要 下载⼀次就缓存起来了,避免了重复下载。

main.js 170kb

Lodash.js 150kb

Main.js 20kb

import _ from "lodash";
console.log(_.join(['a','b','c','****']))

假如我们引⼊⼀个第三⽅的⼯具库,体积为1mb,⽽我们的业务逻辑代码也有1mb,那么打包出来的体积⼤ ⼩会在2mb

导致问题:

  • 体积⼤,加载时间⻓
  • 业务逻辑会变化,第三⽅⼯具库不会,所以业务逻辑⼀变更,第三⽅⼯具库也要跟着变。

其实code Splitting概念 与 webpack并没有直接的关系,只不过webpack中提供了⼀种更加⽅便的⽅法 供我们实现代码分割

基于webpack.js.org/plugins/spl…

详细配置:

optimization: {
    splitChunks: {
      chunks: 'async',//对同步 initial,异步 async,所有的模块有效 all
      minSize: 30000,//最⼩尺⼨,当模块⼤于30kb
      maxSize: 0,//对模块进⾏⼆次分割时使⽤,不推荐使⽤
      minChunks: 1,//打包⽣成的chunk⽂件最少有⼏个chunk引⽤了这个模块
      maxAsyncRequests: 5,//最⼤异步请求数,默认5
      maxInitialRequests: 3,//最⼤初始化请求书,⼊⼝⽂件同步请求,默认3
      automaticNameDelimiter: '-',//打包分割符号
      name: true,//打包后的名称,除了布尔值,还可以接收⼀个函数function
      cacheGroups: {//缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor", // 要缓存的 分隔出来的 chunk 名称
          priority: -10//缓存组优先级 数字越⼤,优先级越⾼
        },
        other: {
          chunks: "initial", // 必须三选⼀: "initial" | "all" | "async"(默认就是async)
          test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk,
          name: "other",
          minSize: 30000,
          minChunks: 1,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true//可设置是否重⽤该chunk
        }
      }
    }
  }

使⽤下⾯配置即可:

optimization:{
 //帮我们⾃动做代码分割
 splitChunks:{
 	chunks:"all",//默认是⽀持异步,我们使⽤all
 }
}

作用域提升 Scope Hoisting

作⽤域提升(Scope Hoisting)是指 webpack 通过 ES6 语法的静态分析,分析出模块之间的依赖关 系,尽可能地把模块放到同⼀个函数中。下⾯通过代码示例来理解:

// hello.js
export default 'Hello, Webpack';
// index.js
import str from './hello.js';
console.log(str);

打包后, hello.js 的内容和 index.js 会分开

通过配置 optimization.concatenateModules=true`:开启 Scope Hoisting

// webpack.config.js
module.exports = {
 optimization: {
 	concatenateModules: true
 }
};

我们发现hello.js内容和index.js的内容合并在⼀起了!所以通过 Scope Hoisting 的功能可以让 Webpack 打包出来的代码⽂件更⼩、运⾏的更快。

HardSourceWebpackPlugin

HardSourceWebpackPlugin 是webpack的插件,用于为模块提供中间缓存步骤。

作用:

  • 提供中间缓存的作⽤
  • ⾸次构建没有太⼤的变化
  • 第⼆次构建时间就会有较⼤的节省

安装:

npm install --save-dev hard-source-webpack-plugin

使用:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const plugins = [
 new HardSourceWebpackPlugin()
]

使用happypack并发执行任务

构建时间较久

项⽬复杂度较⾼

运⾏在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要⼀个⼀个地处理任务,不能同 时处理多个任务。 Happy Pack 就能让Webpack做到这⼀点,它将任务分解给多个⼦进程去并发执⾏,⼦进程处理完后再将结果发送给主进程。从⽽发挥多核 CPU 电脑的威⼒。

安装:

npm i -D happypack

使用:

var happyThreadPool = HappyPack.ThreadPool({ size: 5 });
 //const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
// webpack.config.js
rules: [
    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      use: [
        {
          // ⼀个loader对应⼀个id
          loader: "happypack/loader?id=babel"
        }
      ]
    },
    {
      test: /\.css$/,
      include: path.resolve(__dirname, "./src"),
      use: ["happypack/loader?id=css"]
    },
  ]
  
  //在plugins中增加
  plugins: [
    new HappyPack({
      // ⽤唯⼀的标识符id,来代表当前的HappyPack是⽤来处理⼀类特定的⽂件
      id: 'babel',
      // 如何处理.js⽂件,⽤法和Loader配置中⼀样
      loaders: ['babel-loader?cacheDirectory'],
      threadPool: happyThreadPool,
    }),
    new HappyPack({
      id: "css",
      loaders: ["style-loader", "css-loader"]
    }),
  ]

参考资料: