0
点赞
收藏
分享

微信扫一扫

webpack再体验:性能优化

萧萧雨潇潇 2022-05-06 阅读 70

文章目录


前言

webpack基本使用咱们已经会了,现在咱们一起来聊聊如何提升其性能。


一、HMR(模块热加载)

当我们在打包文件的时候发现,只修改一个模块中的小部分内容也会整个全部重新打包,那么假如我们的项目中有一万个模块,我们只修改了其中一个模块中的内容,完全没有必要全部重新打包,只重新打包这一个模块就好了,这就是我们所说的热加载。

这里在配置devServer是添加一个属性hot:true,可以实现css文件的热加载。

devServer: {
    contentBase: resolve(__dirname, 'build'),
    compress: true,
    port: 3000,
    open: true,
    // 开启HMR功能
    // 当修改了webpack配置,新配置要想生效,必须重新webpack服务
    hot: true
}

修改入口文件配置可以让html文件实现热加载,将html文件路径加上。

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

在入口文件添加实现js的热加载

 // 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
if (module.hot) {
   // 方法会监听 a.js 文件的变化,一旦发生变化,其他模块不会重新打包构建。
    // 会执行后面的回调函数
  module.hot.accept('./a.js', function() {
  
  });
}

二、source_map

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

通过在webpack.config.js文件中最后位置添加如下配置实现

devtool: 'eval-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加入

叫内联和外部
内联:生成的map文件在built.js中,built.js中每个文件后
在这里插入图片描述
外部:单独生成一个map文件
在这里插入图片描述
注意:内联的构建速度比外部快

示例
我们将print.js修改成如下

function print() {
  const content = 'hello print';
  console.log(content)();//这里发生错误
}
export default print;

运行html后直接在控制台报错如图所示,点击红色标注位置可以直接跳转到源码中错误出现位置。
在这里插入图片描述

开发环境配置

开发环境下需要考虑两个方面:速度快,调试更友好

  1. 速度快(eval>inline>cheap>…)
    eval-cheap-souce-map 最快
    eval-source-map 其次
  2. 调试更友好
    souce-map
    cheap-module-souce-map
    cheap-souce-map

** 建议兼顾速度和调试更友好: eval-source-map / eval-cheap-module-souce-map(调试友好不如前者,但速度更快)**

生产环境配置

生产环境需要考虑:源代码要不要隐藏? 调试要不要更友好

  • 内联会让代码体积变大,所以在生产环境不用内联
  • nosources-source-map 全部隐藏
  • hidden-source-map 只隐藏源代码,会提示构建后代码错误信息

建议使用:source-map / cheap-module-souce-map

三、oneOf

当使用webpack打包时每个文件都会过一遍配置文件中rules中的配置选项,看看是否命中,这样做是没必要的,只需要匹配一个就够了。想要达到这样的效果只需要将配置的loader放在oneOf数组中即可,这样当命中一个loader后就不会继续匹配后虚的loader了。

注意:oneOf数组中不能有两个loader处理同一种文件,如果有,则可以把优先使用的loader提取到oneOf数组外边即可。如下所示

 module: {
    rules: [
      {
        // 在package.json中eslintConfig --> airbnb
        test: /\.js$/,
        exclude: /node_modules/,
        // 优先执行
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        // 以下loader只会匹配一个
        // 注意:不能有两个配置处理同一种类型文件
        oneOf: [
          {
            test: /\.css$/,
            use: [...commonCssLoader]
          },
          {
            test: /\.less$/,
            use: [...commonCssLoader, 'less-loader']
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: {version: 3},
                    targets: {
                      chrome: '60',
                      firefox: '50'
                    }
                  }
                ]
              ]
            }
          },
          {
            test: /\.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              name: '[hash:10].[ext]',
              outputPath: 'imgs',
              esModule: false
            }
          },
          {
            test: /\.html$/,
            loader: 'html-loader'
          },
          {
            exclude: /\.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }
        ]
      }
    ]
  }

四、缓存

缓存的思想是在第一次打包后,缓存打包好后的文件,若文件修改则只修改改动的文件。具体怎么实现呢?

babel缓存

只需要在使用babel处理js时添加cacheDirectorv: true

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

文件资源缓存

使用hash值

每次wepack构建时会生成一个唯一的hash值,可以通过hash值判断文件有无重新打包来更新缓存。

问题: 因为js和css同时使用一个hash值。如果重新打包,可能我只改动一个文件,却会导致所有缓存失效。

output: {
	// 给输出文件名添加hash值
    filename: 'js/built.[contenthash:10].js',
    path: resolve(__dirname, 'build')
}

chunkhash

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

问题: js和css的hash值还是一样的,因为css和js都是在build.js中被引入的,所以同属于一个chunk,也就是说这样会使js文件和css文件绑定在一起,那么如果修改css,js也会更新缓存。

output: {
    filename: 'js/built.[chunkhash:10].js',
    path: resolve(__dirname, 'build')
}

contenthash

contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样,让代码上线运行缓存更好使用

output: {
    filename: 'js/built.[contenthash:10].js',
    path: resolve(__dirname, 'build')
}

五、Tree Shaking(树摇)

理解:可以把应用程序想象成一棵树,应用程序会引入第三方库,但是呢我们的应用程序并没有用到所有的第三方库文件,那些没被用到的可以认为是书上的死叶子,通过摇动这棵树来确定哪些是死叶子并去除它,以此来减少应用程序的体积。

使用前提:

  1. 必须使用ES6模块化
  2. 开启production环境

在package.json中配置

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

上面这种配置可能会有问题:可能会把css / @babel/polyfill (副作用)文件干掉,下面这种方法可以避免。

// 被标记的资源就不会被处理
//下面的 * 代表任意的css文件和less文件
"sideEffects": ["*.css", "*.less"]

六、代码分割

理解:正常情况下我们使用webpack打包时,不管源文件有多少个js文件,最终打包完成后都只有一个js文件,分割代码就会打包成多个js文件,最少实现按需加载。

1、多入口文件

entry: {
    // 多入口:有一个入口,最终输出就有一个bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
  },
  output: {
    // [name]:取文件名
    filename: 'js/[name].[contenthash:10].js',
    path: resolve(__dirname, 'build')
  }

上面的配置就会输出index.hash.jstest.hash.js两个文件。

2、optimization

  1. 可以将node_modules中代码单独打包一个chunk最终输出
  2. 只要是多入口一定会打包多个chunck,自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
 optimization: {
    splitChunks: {
      chunks: 'all'
  }
}

3、通过js代码控制

通过js代码,让某个文件被单独打包成一个chunk,import动态导入语法:能将某个文件单独打包

import(/* webpackChunkName: 'test' */'./test')
  .then(({ mul, count }) => {
    // 文件加载成功~
    // eslint-disable-next-line
    console.log(mul(2, 5));
  })
  .catch(() => {
    // eslint-disable-next-line
    console.log('文件加载失败~');
  });

代码分割总结

  1. 单入口默认情况下只输出一个文件,多入口有几个入口输出几个文件。
  2. optimization:如果是单入口,只会分割node_modules文件,如果多入口还会分析有无公共文件,若有则打包成一个文件。
  3. 通过js代码控制。

七、懒加载

触发某些条件以后才加载,而不是一上来就加载。

document.getElementById('btn').onclick = function() {
  // 懒加载~:当文件需要使用时才加载,本例只在按钮点击是才加载。
  import(/* webpackChunkName: 'test'*/'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

正常加载:可以认为是并行加载(同一时间加载多个文件)
预加载 prefetch:等其他资源加载完毕,会在使用之前,浏览器空闲了,再偷偷加载资源。(兼容性不好)

document.getElementById('btn').onclick = function() {
  import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

八、PWA

渐进式网络开发应用程序(离线可访问),使用workbox --> workbox-webpack-plugin

const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
new WorkboxWebpackPlugin.GenerateSW({
      /*
      两项配置:
        1. 帮助serviceworker快速启动
        2. 删除旧的 serviceworker
        生成一个 serviceworker 配置文件~
      */
      clientsClaim: true,
      skipWaiting: true
})

在index.js中注册serviceWorker处理兼容性问题

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(() => {
        console.log('sw注册成功了~');
      })
      .catch(() => {
        console.log('sw注册失败了~');
      });
  });
}

需要注意的是

  1. eslint不认识 window、navigator全局变量
    解决:需要修改package.json中eslintConfig配置
 "eslintConfig": {
"extends": "airbnb-base",
"env": {
      "browser": true// 支持浏览器端全局变量
    }
  }
  1. sw代码必须运行在服务器上
    –> nodejs
    第一步:npm i serve -g
    第二步:serve -s build 启动服务器,将build目录下所有资源作为静态资源暴露出去

九、多进程打包

先下载:thread-loader,需要修改babel-loader配置

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
      'thread-loader',
      {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              useBuiltIns: 'usage',
              corejs: { version: 3 },
              targets: {
                chrome: '60',
                firefox: '50'
              }
            }
          ]
        ]
      }
    }
  ] 
}

开启多进程打包有利有弊因为 进程启动大概为600ms,进程通信也有开销。只有工作消耗时间比较长,才需要多进程打包。

进程数量可控的配置

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'thread-loader',
      options: {
        workers: 2 // 开两个进程打包
      }
    },
    {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              useBuiltIns: 'usage',
              corejs: { version: 3 },
              targets: {
                chrome: '60',
                firefox: '50'
              }
            }
          ]
        ]
      }
    }
  ]
}

十、externals

忽略打包,忽略打包后需要手动的再次引入忽略的文件
例如:

externals: {
    // 拒绝jQuery被打包进来
    jquery: 'jQuery'
}

可以引入CDN

十一、DLL

作用:指示webpack那些库是不参与打包的,dll会单独的对某些库进行单独打包,避免重复打包,将node库打包成一个窗口。

当你运行 webpack 时,默认查找 webpack.config.js 配置文件,但我们需要运行 webpack.dll.js 文件,那么执行一下语句运行webpack

 webpack --config webpack.dll.js

webpack.config.js

const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'built.js',
    path: resolve(__dirname, 'build')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    // 告诉webpack哪些库不参与打包,同时使用时的名称也得变~
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, 'dll/manifest.json')
    }),
    // 将某个文件打包输出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, 'dll/jquery.js')
    })
  ],
  mode: 'production'
};

使用dll技术,对某些库(第三方库:jquery、react、vue…)进行单独打包,需要如下配置
webpac.dll.js

const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // 最终打包生成的[name] --> jquery
    // ['jquery'] --> 要打包的库是jquery
    jquery: ['jquery'],
  },
  output: {
    filename: '[name].js',
    path: resolve(__dirname, 'dll'),
    library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins: [
    // 打包生成一个 manifest.json --> 提供和jquery映射
    new webpack.DllPlugin({
      name: '[name]_[hash]', // 映射库的暴露的内容名称
      path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
    })
  ],
  mode: 'production'
};

单独打包了jquery,并生成了映射关系文件
在这里插入图片描述

总结

webpack性能优化内容比较多并且有点复杂,而且需要尝试才能发现具体的差别,继续加油吧

举报

相关推荐

0 条评论