前端打包优化实战总结几个有效降低bundle体积的方法

Mr-依甜 框架 阅读 2,397
赞 19 收藏
二维码
手机扫码查看
反馈

先搞个最基础的打包优化配置

最近接手了个老项目,打包出来的文件能有20MB,用户加载得要死要活的。打开Chrome DevTools一看,主包chunk-vendors.js就有8MB,这还得了?直接动手优化。

前端打包优化实战总结几个有效降低bundle体积的方法

首先在vue.config.js里加几个最基本的配置:

const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            name: 'chunk-vendors',
            test: /[\/]node_modules[\/]/,
            priority: 10,
            chunks: 'initial'
          },
          elementUI: {
            name: 'chunk-elementUI',
            priority: 20,
            test: /[\/]node_modules[\/]_?element-ui(.*)/
          }
        }
      }
    }
  }
})

这样分包后,至少把第三方库和业务代码分开打包了。elementUI单独拆出来是因为它特别大,经常改动的话用户每次都要重新下载整个vendor包。

代码压缩这块的坑踩够了

压缩这一块我之前踩过不少坑,特别是source map的问题。开发环境一般会开productionSourceMap: true,方便调试错误定位。但上线的时候千万别忘了关掉,不然打包出来的map文件比源码还大。

module.exports = defineConfig({
  productionSourceMap: false, // 记住要关掉
  
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer = [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: true, // 去掉console
              drop_debugger: true // 去掉debugger
            }
          }
        })
      ]
    }
  }
})

这里有个重点,drop_console设置为true后会把所有console都删掉,包括console.error,所以记得提前把重要的错误处理用try-catch包起来,别到时候出了错也看不到日志。

懒加载路由才是王道

路由懒加载这个我就不多说了,基本操作。但有个细节要注意,import()里不能写死路径,要用变量会导致webpack无法静态分析依赖。

// 正确写法
{
  path: '/home',
  component: () => import('@/views/Home.vue')
}

// 错误写法
const viewName = 'Home'
{
  path: '/home',
  component: () => import(@/views/${viewName}.vue)
}

动态导入写不对的话webpack分析不出来模块关系,就不会生成独立的chunk,那就白费了。

CDN外部化是个好办法

对于特别大的库比如vue、vuex、element-ui这些,可以考虑放到CDN上。但配置起来有点麻烦,而且版本管理是个问题。

module.exports = defineConfig({
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.externals = {
        'vue': 'Vue',
        'vue-router': 'VueRouter',
        'vuex': 'Vuex',
        'axios': 'axios',
        'element-ui': 'ELEMENT'
      }
    }
  }
})

然后在public/index.html里手动引入CDN:

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.1/dist/vue-router.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/index.js"></script>

这里有坑,CDN资源加载失败的情况下页面就跑不起来了。建议加个onerror兜底:

<script 
  src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js" 
  onerror="window.location.href='/error.html'">
</script>

图片压缩和Base64转换配置

图片处理这块也很重要,小图标转base64可以减少HTTP请求,大图片就得压缩了。

module.exports = defineConfig({
  chainWebpack: config => {
    // 小于4KB的图片转base64
    config.module
      .rule('images')
      .use('url-loader')
      .loader('url-loader')
      .tap(options => Object.assign(options, { 
        limit: 4096,
        fallback: {
          loader: 'file-loader',
          options: {
            name: 'img/[name].[hash:8].[ext]'
          }
        }
      }))
  }
})

不过这里有个坑,如果项目里图片特别多,全部转base64会让JS包变得很大,反而影响首屏加载速度。一般建议只对小于4KB的小图标这样做。

Tree Shaking该来的还是要来

Tree Shaking主要针对ES6模块语法,确保使用的都是按需导入。像Element UI这样全量引入的库就享受不到tree shaking的好处了。

// 推荐写法
import { Button, Table } from 'element-ui'

// 全量引入就享受不到tree shaking
import ElementUI from 'element-ui'

配合babel-plugin-import插件可以实现按需加载:

// babel.config.js
module.exports = {
  plugins: [
    [
      "import",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

Webpack Bundle Analyzer帮你找问题

分析打包结果必备工具,安装webpack-bundle-analyzer后可以直观看到各个模块的大小分布。

npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = defineConfig({
  configureWebpack: config => {
    if (process.env.ANALYZE) {
      config.plugins.push(new BundleAnalyzerPlugin())
    }
  }
})

然后执行npm run build –report,就会自动打开可视化界面显示打包结果。这个工具帮我发现了不少隐藏很深的大体积依赖。

缓存策略配置细节

合理配置文件名哈希可以有效利用浏览器缓存。每次构建只有真正改动的文件才会改变哈希值,没变的就能走缓存了。

module.exports = defineConfig({
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.output.filename = 'js/[name].[contenthash:8].js'
      config.output.chunkFilename = 'js/[name].[contenthash:8].js'
    }
  }
})

contenthash比chunkhash更精细,只有文件内容变了hash才会变。不过注意如果用了externals,外链资源的版本更新可能不会触发hash变化,需要自己控制CDN版本。

以上是我踩坑后的总结,希望对你有帮助。打包优化的细节还有很多,像预加载preload、资源提示prefetch这些,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论