我的webpack学习之路(关于webpack的性能优化)

1、我的webpack学习之路(webpack初步认知和webpack基础打包)
2、我的webpack学习之路(webpack基础开发配置)
3、我的webpack学习之路(webpack配置详解)

4、关于我的学习笔记

文章目录

  • 1、关于webpack的性能优化
  • 2、开发环境打包构建优化(HMR)
  • 3、开发环境调试优化(source-map)
  • 4、生产环境构建优化(oneOf)
  • 5、生产环境构建优化(缓存)
  • 6、生产环境构建优化(tree shaking)
  • 7、代码分割(code split)
  • 8、懒加载和预加载(lazy loading)
  • 9、PWA
  • 10、多进程打包
  • 11、externals
  • 11、dll(动态链接库)
  • 0、学习所有用到依赖

1、关于webpack的性能优化

主要分为两个方面来处理:

  • 开发环境性能优化
  • 生产环境性能优化

开发环境性能优化:

  • 优化打包构建速度
    • HMR
  • 优化代码调试
    • source-map

生产环境性能优化:

  • 优化打包构建速度
    • oneOf
    • babel缓存
    • 多进程打包
    • externals
    • dll
  • 优化代码运行的性能
    • 缓存(hash、chunkhash、contenthash)
    • tree shaking
    • code split
    • 懒加载/预加载
    • pwa

2、开发环境打包构建优化(HMR)

在以前学习了webpack-dev-server,用它来实现代码修改热更新,但是有一个问题是,当我们只修改了其中一个模块时(某个css\某个js),就会将所有的模块全部重新构建,影响构建速度,所以要用到HMR

HMR: hot module replacement 热模块替换/模块热替换

作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块)极大提升构建速度

实现HMR其实很简单,只需要在devServer中添加hot:true即可

// webpack.config.js
devServer: {
     
	...
	hot: true, // 开启HMR功能
}

问题:
1、修改样式文件时,HMR正常使用,只重新构建了修改过的css文件,因为style-loader内部实现了HMR功能~
2、js文件:发现js文件默认不适用HMR功能
解决方法:需要修改js代码,添加支持HMR功能的代码

import print from './print.js'
// 会全局寻找module这个对象,查看hot热更新(HMR)是否启用 ---> 让HMR功能代码生效
if(module.hot) {
     
	module.hot.accept('./print.js', function{
     
	// 方法会监听print.js文件的变化,一旦发生变化,其他模块不会打包构建,会执行后面的回调函数
		print()
	})
}

注意:HMR功能对js处理,只能处理非入口js文件的其他文件(入口文件改变,则必然全都重新构建)

3、HTML文件:也是默认不支持HMR功能的,但会导致htmlwe文件不能热更新了~~
解决办法:修改entry入口,将html文件引入

entry: ['./src/js/index.js', './src/index.html'],

思考:html文件需不需要HMR呢?
html文件对应来讲只有一个html文件,在看下HMR的作用,一个模块发生变化,只会重新打包这一个模块。不像js文件有很多个模块,其中一个变其他不变。html文件变化,只能变化着一个文件,所以没必要使用HMR功能

3、开发环境调试优化(source-map)

source-map: 一种提供源代码到构建后代码映射技术(如果构建后代码出错,通过映射可以追踪源代码错误)

配置方法是在webpack.config.js中增加一个配置devtool:'source-map’

devtool: 'source-map'

如上配置,就配置好了一个调试优化,执行webpack命令后,会发现和build.js同一目录下多了一个map 文件,这个就是所谓的source-map文件,提供源代码到构建后代码映射关系

devtool:有几个参数: [ inline- |hidden -| eval- ] [ nosources- ] [ cheap - [ module - ] ] source-map

参数 map文件生产方式 介绍
source-map 外部 错误代码的准确信息 和 源代码的错误位置
inline-source-map 内联 只生成一个内联source-map;错误代码的准确信息 和 源代码的错误位置
hidden-source-map 外部 错误代码的错误原因,但是没有错误位置; 不能追踪到源代码错误,只能提示到构建后代码的错误位置
eval-source-map 内联 每个文件狗生成对应的source-map,都在eval;错误代码的准确信息 和 源代码的错误位置
nosources-source-map 外部 错误代码的准确信息,但是没有任何源代码信息
cheap-source-map 外部 错误代码的准确信息 和 源代码的错误位置;只能精确到行
cheap-module-source-map 外部 错误代码的准确信息 和 源代码的错误位置;module会将loader的source-map加入

内联:sorce-map文件是和build.js合二为一
外部:在build.js同级生产map文件

内联 和 外部的区别: 1、外部生成了文件,内联没有。2、内联构建速度更快

   开发环境:速度快一点,调试更友好
     速度快(eval> inline>cheap>...)
      		eval-cheap-souce-map
     	 	eval-source -map
      调试友好
        	source-map
        	cheap-moudle-sourec-map
        	cheap-sourec-map
        
      开发环境最好的选择:---> eval-source-map / eval-source-cheap-moudle-map
    生产环境:源代码要不要隐藏?调试要不要更友好
      内联会让代码体积非常大,所以生产环境不用内联
      	nosources-source-map 全部隐藏
      	hidden-source-map 只隐藏源代码,会提示构建后代码错误

    生产环境最好的选择:---> source-map / cheap-moudle-sourec-map
   

4、生产环境构建优化(oneOf)

​ 首先配置是很简单,如下

module: {
     
    rules: [
        {
     
            oneOf: [
				// 对应loader配置
            ]
        }
    ]
}

为什么使用oneOf?

​ 在我原先写loader的时候,在rules中有非常多的loader规则,一个文件要被所有的loader都过一遍,有些loader处理不了,有些loader则会被命中,这样就不太好。配置oneOf能让loader处理性能更好(oneOf主要是提升构建速度,是文件不会被多个loader反复都过一遍)

oneOf的意思:以下loader只会匹配一个

注意:不能有两项配置,处理同一类型的文件,如下,js类型文件有两种配置,只会生效一个loader配置。

解决办法:将eslint-loader提取出去,如下。这样rules内的两个loader都会执行,配置了enforce: ‘pre’,也会优先执行

module: {
     
    rules: [
        {
     
            test: /\.js$/,
            exclude: /node_modules/, // 排除node_modules,
            loader: 'eslint-loader',
            // 设置优先执行
            enforce: 'pre',
            options: {
     
              fix: true
            }
        },
        {
     
            oneOf: [
				//{
     
                  //  test: /\.js$/,
                   // exclude: /node_modules/, // 排除node_modules,
                   // loader: 'eslint-loader',
                    // 设置优先执行
                   // enforce: 'pre',
                   // options: {
     
                     // fix: true
                    //}
                  //},
                  // js兼容处理..
                  {
     
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader',
                    options: {
     
                      presets: [
                        [
                          '@babel/preset-env',
                          {
     
                            // 按需加载
                            useBuiltIns: 'usage',
                            corejs: {
     version: 3},
                            targets: {
     
                              chrome: '60'
                            }
                          }
                        ]
                      ]
                    }
                  }
            ]
        }
    ]
}

5、生产环境构建优化(缓存)

从两点出发来设置缓存:1、babel缓存;2、整体资源缓存

1、babel缓存是什么意思呢?

​ 我们写代码的时候,永远是js代码是最多的,结构和样式没有什么办法做更好的处理,为什么对babel处理,因为babel对js进行编译处理,编译成浏览器能识别的语言(js兼容性处理)。假设100个js模块,只改了1个js文件,不可能将全部文件重新编译处理,应该是不变的,类似前面的HMR功能,一个某块变,其他模块不变。生产环境先不能是使用webpack-dev-server所以不能使用HMR功能。配置如下,只需要在babel-loader中options添加一个熟悉

{
     
    test: /\.js$/,
        exclude: /node_modules/,
            loader: 'babel-loader',
                options: {
     
                    // 开启babel缓存
                    // 第二次构建时,才会读取缓存
                    cacheDirectory: true,
                    presets: [
                        [
                            '@babel/preset-env',
                            {
     
                                // 按需加载
                                useBuiltIns: 'usage',
                                corejs: {
     version: 3},
                                targets: {
     
                                    chrome: '60'
                                }
                            }
                        ]
                    ]
                }
}

2、文件资源缓存

​ 怎么查看缓存呢?首先先写一个简单的服务器代码server.js

// server,js
/*
	服务器代码
	通过node server.js运行
	
	访问服务器地址
		http://loacalhost:3000
*/
const express = require('express')

const app = express()

app.use(express.static('build', {
     maxAge: 1000 * 3600}))

app.listen(3000)

我的webpack学习之路(关于webpack的性能优化)_第1张图片
我的webpack学习之路(关于webpack的性能优化)_第2张图片

当我修改了代码是,访问的还是上一次缓存的内容,修改的代码没有生效

可以通过文件名添加版本号(hash值)的方式来解决这个问题(如果资源名称没有变就走缓存,如果变了就会重新请求资源文件)

hash:每次webpack构建时会生成一个唯一的hash值

...

module.exports = {
     
  entry: './src/js/index.js',
  output: {
     
    filename: 'js/build.[hash:10].js',
    path: resolve('build')
  },
  module: {
     
    ...
  },
  plugins: [
    new MiniCssExtractPlugin({
     
      filename: 'css/build.[hash:10].css'
    }),
    ...
  ],
  mode: 'production'
}

如上配置,将输出的文件名添加10位的hash值,但是这样会有一个问题:因为css文件和js文件是共享的webpack打包生成的hash值,一旦发生变化css文件和js文件是一起变的,修改一个都会变,缓存就失效了。webpack又引入了另一个hash值chunkhash

chunkhash:根据chunk生成的hash值,如果打包来源同一个chunk,那么hash值就一样

...
module.exports = {
     
  entry: './src/js/index.js',
  output: {
     
    filename: 'js/build.[chunkhash:10].js',
    path: resolve('build')
  },
  module: {
     
    ...
  },
  plugins: [
    new MiniCssExtractPlugin({
     
      filename: 'css/build.[chunkhash:10].css'
    }),
    ...
  ],
  mode: 'production'
}

这里打包会发现js和css的hash值还是一样的,因为css是在js中被引进来的,所以同属于一个chunk。因为这些都被引入到同一个入口文件,所有根据入口文件引入的都会生成一个chunk。

所以最后的解决方法就是通过contenthash来解决

contenthash: 根据文件的内容来生成hash值。不同的hash值一定不一样

...
module.exports = {
     
  entry: './src/js/index.js',
  output: {
     
    filename: 'js/build.[contenthash:10].js',
    path: resolve('build')
  },
  module: {
     
    ...
  },
  plugins: [
    new MiniCssExtractPlugin({
     
      filename: 'css/build.[contenthash:10].css'
    }),
    ...
  ],
  mode: 'production'
}

这样只需要控制hash值的变化来控制资源更新

babel缓存:让第二次打包构建速度更快

文件资源缓存:让代码上线运行缓存更好用

6、生产环境构建优化(tree shaking)

tree shaking: 去除无用代码

使用前提:

1、必须使用es6模块化

// test.js
export function a(x, y) {
     
  return x * y;
}

export function b(x, y) {
     
  return x - y;
}

// index.js
import {
      a } from './test'
console.log(a(2,3))

2、开启production环境

mode: 'production'

作用:减少代码体积

满足以上条件后重新构建打包,在打包过程中会自动启动tree shaking。打包完成后查看build.js会发现test.js内b方法的相关内容没有了

使用的时候还要注意一个小问题,在不同版本tree shaking会有差异,会无意之间将css文件当做为经引用的代码干掉

模拟测试,在package.json中添加**“sideEffects”: false**

“sideEffects”: false : 所有代码都是没有副作用的代码(都可以进行tree shaking)

再构建一次,会发现没有css资源,这样写会把css资源干掉

解决办法: “sideEffects”: ["*.css"]

/* 
  tree shaking: 去除无用代码
    前提:1、必须使用es6模块化。2、开启production环境
    作用:减少代码体积

    在package.json中配置
      "sideEffects": false 所有代码都没有副作用(都可以进行 tree shaking)
        问题:可能会吧css / @babel/polyfill (副作用)文件干掉
      "sideEffects": ["*.css","*.less"],这里标记不被tree shaking的文件
*/

7、代码分割(code split)

code spli:将打包输出的一个文件(chunk)分割成多个文件

webpack代码分割大致分为三种方式:1、定义多入口文件。2、使用optimization配置插件。3、通过js

代码让某个文件被单独打包成一个chunk

1、定义多入口文件

​ entry以往的使用是写一个入口文件地址

entry: './src/index.js'

如上就是配置单入口,倘若在index.js文件中引入其他文件,那么最终打包只会生产一个文件

// index.js
import test from './test'
console.log('hello index')

通过entry配置多入口文件

entry: {
     
    main: './src/index.js',
    test: './src/test.js'
}

如上配置多入口文件,在运行打包时,会看到打包出了两个文件,同时也不需要在入口文件中引入test.js文件

打包后的两个文件都是build.hash.js比较难区分,可以同配置output来设置文件名

output: {
     
    // 设置[name],取文件名
    filename: 'js/[name].[contenthash:10].js',
    path: resolve('build')
}

2、使用optimization配置插件

作用:可以将node_modules中的代码单独打包成一个chunk输出;自动分析多入口chunk中,有没有公共的文件,如果有会打包成单独一个chunk,不会重复打包

plugin: [...],
optimization: {
     
         splitChunks:{
     
         	chunks: 'all'
         }
}

3、通过写js代码的方式打包

如果entry只设置了一个入口,在单入口文件中通写入js代码(import)的方式引导打包

//index.js
import('./test') // 这里返回的是promise对象
	.then((res) => {
     
    	// res为test导出对象结果
    	console.log('导入成功')
	})
	.catch(() => {
     
    	console.log('导入失败')
	})

如上配置,运行打包后,也会各自生产两个打包文件,还要一个命名问题,test打包生成的文件名是根据打包顺序命名的可能为123.hash.js。可以通过import内添加注释,设置文件名

import(/* webpackChunkName: 'test' */'./test')

如上配置运行打包后,打包文件就会是名为test的文件

8、懒加载和预加载(lazy loading)

懒加载和预加载通过js代码实现,与webpack配置无关,与代码分割(code split)有一定的相似之处

正常情况下

// index.js
console.log('index.js文件被加载')
import {
     a} from './test.js'
console.log(a)
export function a(x, y) {
     
  return x * y;
}
console.log('test.js被加载')

运行打包后看下,两个文件被先后加载:
我的webpack学习之路(关于webpack的性能优化)_第3张图片

懒加载: 当文件需要使用时才加载

<button id="btn"> 点击 button>
//index.js
console.log('index.js文件被加载')
document.getElementById('btn').onclick = function() {
     
    import(/* webpackChunkName: 'test' */'./test').then(({
     a}) => {
     
        console.log(a(2,3))
    }}

在运行打包后,可以看到被index,test被打包成两个chunk,类似代码分割,只有在点击事件生效时,才会加载test文件,重复点击时,也只会加载一次,方法同样事项

预加载:会在使用之前,提前加载js文件

加上一个配置webpackPrefetch

//index.js
console.log('index.js文件被加载')
document.getElementById('btn').onclick = function() {
     
    import(/* webpackChunkName: 'test',webpackPrefetch: true */'./test').then(({
     a}) => {
     
        console.log(a(2,3))
    }}

打包后打开html文件,会发现点击事件没有生效,但是已经加载了,点击事件生效后,从缓存中拿test文件使用,不会重新加载

正常加载可以认为是并行加载(同一时间加载多个文件)

预加载:prefetch: 等其他资源加载完毕,浏览器空闲了,在偷偷加载。(预加载存在很大兼容问题,最好只在高版本或者移动端使用)

9、PWA

PWA: 渐进式网络开发应用(离线可访问)使用serviceworker实现

webpack实现PWA需要借助一个插件workbox-webpack-plugin

npm安装后在webpack中配置

const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
...
Plugins: [
    ...
    new WorkboxWebpackPlugin.GenerateSW({
     
        /* 
        1.帮助serviceworker快速启动
        2.删除旧的serviceworker

        生成一个serviceworker 配置文件 ~
      */
     clientsClaim: true,
     skipWaiting: true
    })
]

webpack文件配置好后,配置入口文件注册serviceWorker

//index.js
// 应为serviceworker有兼容性问题,以下是简单兼容性处理,是否支持serviceworker
if('serviceWorker' in navigator) {
     
  window.addEventListener('load', () => {
     
    navigator.serviceWorker.register('/service-worker.js')
      .then(() => {
     
        console.log('sw注册成功')
      })
      .catch(() => {
     
        console.log('sw注册失败')
      })
  })
}

注意:eslint 不认识window、navigator 全局变量

解决: 需要修改package.json 中eslintConfig配置

“env”: {

“browser”: true // 支持浏览器端全局变量

}

sw代码必须运行在服务器上:两种运行服务方法

—> nodejs

—>

​ npm i server -g

​ serve -s build 启动一个服务器,将build目录下所有资源作为静态资源暴露出去

如上就完成了pwa的简单配置

10、多进程打包

js主线程是单线程的,它同一时间只能干一件事,事情比较多,就要排队等前一个任务结束,在继续;所以通过多进程来优化打包速度

首先需要下载一个loader thread-loader

thread-loader放在某一个loader的后面,就会对其开启多进程打包

thread-loader一般是给babel-loader使用,使用方式如下

//webpack.config.js
{
     
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
                // 开启多进程打包(babel工作的时候就会开启多进程)
                /*
                	开启多进程打包是有利有弊的(合理使用)
                	进程启动大概600ms,进程通信也有开销(时间)
                	只有工作消耗时间比较长,才需要多进程
                	一般来说js代码比较多,消耗时间比较长
                	
                	启动进程数(cpu核数-1)
                	
                */
                //'thread-loader',
                //如下可做调整
                {
     
                    loader: 'thread-loader',
                    options: {
     
                		workers: 2 // 进程2个
                	}
                },
                {
     
                    loader: 'babel-loader',
                    options: {
     
                      presets: [
                          [
                              '@babel/preset-env',
                              {
     
                                  // 按需加载
                                  useBuiltIns: 'usage',
                                  corejs: {
     version: 3},
                                  targets: {
      chrome: '60' }
                              }
                          ]
                        ],
                        // 开启babel缓存
                        // 第二次构建时,会读取之前的缓存
                        cacheDirectory: true
                     }
            	]
            }
          }

11、externals

作用:防止将某一些包打包到我们最终输出的bundle

假设通过cdn链接引入jQuery,可以通过externals禁止,不会被打包了

//webpack.config.js
mode: 'production',
externals: {
     
    // 拒绝jqeruy包被打包进来
    // 忽略库名: 'npm包名'
    jquery: 'jQuery'
}

11、dll(动态链接库)

类似externals,会指示webpack那些库是不参与打包的,不同的是dll会单独对某些库进行单独打包,将多个库打包成一个chunk

node_modules内的某些库比较大,正常打包的话会被打包成一个文件,这样文件体积增大。通过dll将这些库单独拆开,打包成不同的chunk,更有利于性能优化

dll打包后能,webpack运行打包后不会重复打包第三方依赖库,提高效率

首先定义一个webpack.dll.js

//webpack.dll.js
/* 
  使用dll技术,对某些库(第三方库:jq,react,vue)进行单独打包

    当你运行 webpack 的时候,默认查找 webpack.config.js 配置文件
    需求: 运行 webpack.dll.js 文件
        --> webpack --config webpack.dll.js
*/
const path = require('path')
const webpack = require('webpack')

function resolve(dir) {
     
  return path.join(__dirname,dir)
}
module.exports = {
     
  entry: {
     
    // 最终打包生成 [name] ---> jquery
    // ['jquery',...] ---> 要打包的库是jquery
    jquery: ['jquery']
  },
  output: {
     
    filename: '[name].js',
    path: resolve('dll'),
    library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins:[
    // 打包生成 manifest.json ----> 提供和jquery的映射关系(通过这个映射关系知道jquery不需要打包)
    new webpack.DllPlugin({
     
      name: '[name]_[hash]', // 映射库的暴露的内容名称
      path: resolve('dll/manifest.json') // 输出的名称
    })
  ],
  mode: 'production'
}

其次配置webpack.config.js,告诉它那些包不参与打包

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

function resolve(dir) {
     
  return path.join(__dirname,dir)
}

....
plugins: [
    // 处理html结构
    new HtmlWebpackPlugin({
     
      template: './src/index.html'
    }),
    // 告诉webpack那些库不参与打包,同时使用时的名称也要变
    new webpack.DllReferencePlugin({
     
      manifest: resolve('dll/manifest.json')
    }),
    // 将某个文件打包输出出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
     
      filepath: resolve('dll/jquery.js')
    })
  ],

0、学习所有用到依赖

"devDependencies": {
     
    "@babel/core": "^7.12.3",
    "@babel/polyfill": "^7.12.1",
    "@babel/preset-env": "^7.12.1",
    "add-asset-html-webpack-plugin": "^3.1.3",
    "babel-loader": "^8.1.0",
    "core-js": "^3.6.5",
    "css-loader": "^5.0.0",
    "eslint": "^7.12.1",
    "eslint-config-airbnb-base": "^14.2.0",
    "eslint-loader": "^4.0.2",
    "eslint-plugin-import": "^2.22.1",
    "file-loader": "^6.2.0",
    "html-loader": "^1.3.2",
    "html-webpack-plugin": "^4.5.0",
    "jquery": "^3.5.1",
    "less": "^3.12.2",
    "less-loader": "^7.0.2",
    "mini-css-extract-plugin": "^1.2.1",
    "optimize-css-assets-webpack-plugin": "^5.0.4",
    "postcss-loader": "^4.0.4",
    "postcss-preset-env": "^6.7.0",
    "style-loader": "^2.0.0",
    "url-loader": "^4.1.1",
    "webpack": "^4.44.2",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0",
    "workbox-webpack-plugin": "^5.1.4"
  },

你可能感兴趣的:(前端,学习,webpack,前端)