基于Vue+webpack之H5打包资源优化

前言

基于公司的业务以及今年接触到的项目大部分都是APP混合开发,即原生Android/ios +H5页面开发APP。项目从产品需求的评审到方案的评审再到开发提测...这一套流程下来让我收货颇多。总想找个时间好好记录一番,大概还是自己懒惰了,一直拖到现在。想记录的东西太多了,一次讲完也没有突出的重点,我今天最想记录的是关于H5资源打包优化的问题。以前常常看到一些大V长篇大论的在讲各种优化问题,自己没有真正动手去实践过的话也不明所以,也不明白优化的重要性。现在还真想感叹一句:小小的优化,真的能大大提高用户体验,何乐而不为呢嘿嘿~,好了,废话说的有点多,接下来我们就直接进入主题吧

现有的问题

基于我实际基础的混合APP开发中H5打包资源,得出了如下问题结论:

1.第三方库为分包,全部资源打包成一个js文件,导致文件过大。虽然说现在手机的更新迭代很快,在性能好的手机上能够快速加载,但是开发APP,我们肯定也是需要兼容一些老旧的设备机的,那么在性能较差的设备,加载的时间就会过长,甚至会出现等待时间过长而显示白屏的情况。

2.第三方组件库没有按需导入

  • Vant组件库全引入,全资源包200kb+
  • Echart全引入,全资源包 800kb+

3.引入的图片没有进行压缩处理

4.过多的注释代码没有被移除,全部打包到了静态资源中

准备工作

由于我的项目中用到的打包工具是webpack,所以这里引入webpack-bundle-analyzer第三方插件,以方便分析打包资源各个模块的占比情况。

webpack-bundle-analyzer插件是什么?

webpack-bundle-analyzer是webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件可读取输出文件夹(通常是dist)中的state.json文件,把该文件可视化展示,生产代码分析本报告,可以直观地分析打包出的文件有哪些,以及它们的大小、占比情况,各文件Gzippped后的大小、模块包含关系、依赖项等。从而我们可以从其中的数据进行分析,做出对应的优化,从而帮助提升代码质量和软件性能。

安装

bash 复制代码
# NPM 
npm install --save-dev webpack-bundle-analyzer
# Yarn 
yarn add -D webpack-bundle-analyzer

使用方法

webpack-bundle-analyzer的使用方式可以分为两种 ,分别是作为插件使用和作为一个cli的一个工具使用,我这里主要将其作为插件的使用方式

作为插件使用

1.在vue.config.js中配置webpack-bundle-analyer

javascript 复制代码
....
chainWebpack(config){
  ....
  if (process.env.NODE_ENV === "development") {
    config
      .plugin("webpack-bundle-analyzer")
      .use(require("webpack-bundle-analyzer").BundleAnalyzerPlugin);
  }
  ....
}

2.运行npm run serve/dev 查看可视化的静态资源包

  • 分析依赖包
  • 移除 无用依赖
  • 抽取三方依赖

从上图我们也可以看出,暴露出的问题也就是我在文章最开始的时候说的那几个问题,接下来为我们将对症下药,针对项目中存在的问题进行优化

优化方案

第三方库使用CDN加载

我们都知道,使用@vue/cli脚手架构建Vue的全家桶项目,打包后会吧vue、vue-router、axios、vuex、第三方组件库例如vant、echart等打包在一起,导致基础chunk、vender包体积特别大,有时一个文件能达到3-5mb,这会大大影响首次加载速度,因此需要抽离第三方公共库,配合CDN加速。

首先我们先看一下,我在项目中所用到的依赖:

分析: 项目整体使用了vant、axios、数据可视化引入了Echart等,这些库本身体积就不小,打包到一起后体积更大

优化配置:

为了方便以后管理,将CDN相关所有配置写入cdn.config.js(与vue.config.js同级)

1.cdn.config.js配置

javascript 复制代码
module.exports = {
  // 是否使用cdn
  useCDN: true,
  // key是'包名', value是静态资源引入后全局的名称 import Vue from 'vue'
  // 忽略打包的第三方库
  externals: {
    'vue': 'Vue',
    'vuex': 'Vuex',
    'vue-router': 'VueRouter',
    'axios': 'axios',
    'echarts': 'echarts',
    // 必须是ELEMENT,否则会报'elementUI is not defined'
    'vant': 'vant'
  },
   //通过cdn方式引入
  cdn: {
    // CDN链接地址:https://www.jsdelivr.com/
    css: [
      //由于项目中没有引入第三方css,所以这里举例的是element-ui的css
      'https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/theme-chalk/index.css'
    ],
    js: [
      'https://cdn.jsdelivr.net/npm/vue@2.6.11',
      'https://cdn.jsdelivr.net/npm/vue-router@3.2.0/dist/vue-router.min.js',
      'https://cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js',
      'https://cdn.jsdelivr.net/npm/echarts@5.2.1/dist/echarts.min.js',
      'https://cdn.jsdelivr.net/npm/vant@2.15.3/lib/vant.min.js',
      'https://cdn.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js','
    ]
  }
}

注意:上方的js文件cdn连接也可以通过下载下来(下载压缩版)放在public文件夹中

2.配置vue.config.js第三方库的externals、设置生成html

javascript 复制代码
const cdnConfig = require('./cdn.config.js');
const isProduction = process.env.NODE_ENV === 'production';

....
configureWebpack:{
  ....
  // 打包忽略以下第三方库
  externals: isProduction && cdnConfig.useCDN ? cdnConfig.externals : {}
  ....
}
...
chainWebpack(config){
  ....
  // ============注入cdn start============
  config.plugin('html')
    .tap(args => {
      args[0].minify.removeAttributeQuotes = false; // 引入打包的双引号,否则本地静态资源读取不到
      // 生产环境或本地需要cdn时,才注入cdn
      if (isProduction) args[0].cdn = cdn
      return args;
    })
  // ============注入cdn end============
  ....

3.设置public/index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
 
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <!-- 使用CDN的CSS文件 -->
    <% for (var i in htmlWebpackPlugin.options.cdn &&
      htmlWebpackPlugin.options.cdn.css) { %>
      <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
      <% } %>
    <!-- 使用CDN的CSS文件 -->
    <title>APP</title>
  </head>
  <body>
    <noscript>
      <strong>errorTip</strong>
    </noscript>
    <div id="app"></div>
 
    <!-- 使用CDN的JS文件 -->
    <% for (var i in htmlWebpackPlugin.options.cdn &&
      htmlWebpackPlugin.options.cdn.js) { %>
      <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
      <% } %>
    <!-- 使用CDN的JS文件 -->
    <!-- built files will be auto injected -->
   </body>
</html>

使用splitChunks进行代码分割

在说splitChunks之前,我们需要明白module、chunk和bundle这三个名词是什么意思:

  • module:就是js的模块化webpack支持commonJs、ES6等模块化规范,简单来说就是你通过import语句引入的代码
  • chunk:chunk是webpack根据功能拆分出来的,包含三种情况
    • 你项目的入口(entry)
    • 通过import()动态引入的的代码
    • 通过splitChunks拆分出来的代码,chunk包含 module,可能一对多也可能是一对一
  • bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出

为了避免一次加载过大的文件和充分利用浏览器缓存,我们需要将不经常被修改的文件单独打包,即对项目中的代码进行分割。其中,第三方依赖库,通用组件、静态资源、工具库等都可以金秀贤代码分割。在webpack中,不进行配置的话默认打包是将所有业务代码和第三方库都是打包到bundle.js文件中的。预想将代码或者第三方库在打包的时候进行分割处理,那么就要用到optimization.splitChunks配置项,不同的配置方式将得到不同的代码分割效果。

javascript 复制代码
....
chainWebpack(config) {
  ....
  config.optimization.splitChunks({
    chunks: "all",//表示选择哪些模块进行优化。有效值为all、async和initial,默认是仅对异步加载的块进行分割
    minSize: 100000, // 模块大于minSize时才会被分割出来。默认100k
    maxSize: 200000, // 生成的块的最大大小,如果超过了这个限制,大块会被拆分成多个小块。
    minChunks: 1, // 拆分前必须共享模块的最小块数。
    maxAsyncRequests: 5, // 按需加载时并行请求的最大数目。
    maxInitialRequests: 3, // 入口点的最大并行请求数
    automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称(例如vendors~main.js)生成名称。此选项允许您指定要用于生成的名称的分隔符。
    automaticNameMaxLength: 30, // 允许为SplitChunksPlugin生成的块名称的最大长度
    cacheGroups: {
      libs: {
        // 第三方库
        name: "chunk-libs",
        test: /[\\/]node_modules[\\/]/,// 控制此缓存组选择的模块,省略它将选择所有模块,它可以匹配绝对模块资源路径名称,匹配快名称时,将选择模块中所有模块
        priority: 10, // 一个模块可以属于多个缓存组,模块出现在优先级最高的缓存组中
        chunks: "initial", // 只打包初始时依赖的第三方
      },
      commons: {
        // 公共模块包
        name: "chunk-commons",
        test: resolve("src/components"),
        minChunks: 3, //  minimum common number
        priority: 5,
        reuseExistingChunk: true,// 如果当前快包含已经从主包中分离出来的模块,那么该模块将被重用,而不是生成新的模块
      },
    },
  });
  ....
}
....

使用代码压缩钱打包的文件

使用代码分割后打包后的文件

按需引入第三方组件库

压缩图片

在webpack的思想中,一切皆模块,所有模块、样式、图片等等这些资源都是模块、因为这些资源也具备模块的特性--他们都复制特定的职能,并且具有可复用性。因此,我们可以使用webpack去管理所有这些资源,并且它们都当作模块来处理。

静态资源指前端中常用的图片,富媒体(Video、Audio)、字体文件等。webpack中静态资源也可以作为模块直接使用的。webpack提供了很多插件和loader对图片进行压缩、合并(CSS Sprite)。webpack还会使用url-loader等插件,将较小的资源通过Base64的方式引入。当项目足够大了之后,配置太多的静态资源处理流程也会影响webpack的打包速度。

针对图片资源,通常有jpg|jpeg|png|gif|ico等格式,静态资源指前端中常用的图片,针对大图片通常使用image-webpack-loader插件压缩一下、小图片使用url-loader转成base64,并比较前后优化差别。

url-loader的使用

首先,url-loader和image-webpack-loader都依赖于file-loader,file-loader简言之就是一个资源加载模块,去找文件资源的loader,然后也可以给静态资源生成哈希值,即唯一识别身份证。我们主要通过url-loader和image-webpack-loader做相关对应的配配置。

1 安装url-loader

javascript 复制代码
npm i url-loader file-loader --save

2 使用url-loader转成base64格式的效果

image-weback-loader的使用

1.安装image-weback-loader

注意点:若有报错大家可以下载指定的稳定版本以使用

bash 复制代码
npm install image-webpack-loader --save-dev

2.压缩前后效果对比

两个loader的完整配置

我这里使用vue项目,所以在配置文件vue.config.js中的chainWebpack加上以下代码即可

javascript 复制代码
chainWebpack(config) {
    config.module.rule("images").test(/\.(jpg|jpeg|png|gif|ico)$/) // 给这些图片类型做压缩
        .use("url-loader") // url-loader要搭配file-loader做图片压缩
        .loader("url-loader")
        .options({
            limit: 1024 * 12,// 小于12kb的图片压缩成base64,图片太大转成base64反而不太合适
            name: "static/img/[name].[ext]"//指定打包后的图片存放的位置,一般放在static下img文件夹里 name.ext分别为:文件名.文件后缀(按照原图片名)
        })
        .end() // 返回上一级 以便于继续添加loader
        .use('image-webpack-loader')
        .loader("image-webpack-loader")
        .options({
            disable: process.env.NODE_ENV == 'development' ? true : false, // 开发环境禁用压缩,生产环境才做压缩,提升开发调试速度
            mozjpeg: { quality: 60 }, // 压缩JPEG图像,压缩质量quality为60,范围0到100
            optipng: { enabled: true }, // 压缩PNG图像,enabled为true开启压缩
            pngquant: { quality: [0.65, 0.90], speed: 4 }, // 质量区间和速度就使用默认值吧
            gifsicle: { interlaced: false }, // Interlace gif for progressive rendering 默认false
            webp: { quality: 60 } // 压缩webp图片,压缩质量quality为60,范围0到100
        })
        .end() // 返回上一级 继续添加loader
        .enforce('post') // 表示先执行配置在下面那个loader,即image-webpack-loader
},

Tree Shaking

Tree Shaking指的是当我们引入一个模块的时候,我不引入这个模块的所有代码,我只因日我需要的代码,那么就需要Tree Shaking这个功能来帮我们实现。

官方有标准的说法:Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)

在weebpack项目中,有一个入口文件,这个入口文件就相当于一棵树 的主干,入口文件中有很多依赖的模块,相当于树枝5.实际情况下,虽然依赖了某个模块,但其实只使用了其中的某些功能。通过Tree-Shaking,将没有使用的模块摇掉,以达到删除无用代码的目的。

而webapck5已经自带了这个功能,当打包环境是production时,默认开启tree-shaking,若想在开发模式下配置tree shaking,在vue.config.js中加上如下即可

javascript 复制代码
optimization: {
    usedExports: true
}

注意:也可以通过sideEffects设置不需要被tree shaking的模块,那么我们肯定会想到,为什么需要对一些特定的模块不做哦tree-shaking处理呢?不是所有的模块都精简删除无用的代码是最好的效果吗?其实不是的。因为在项目中,我们肯定也会自定义去规划一些css文件,但是由于css文件没有导出任何模块,那么就有可能在打包的时候该引入的模块就被摇晃掉了,导致bug,那么此时我们就可以在package.json中设置如下,即匹配到的任何css文件都不进行Tree Shaking

webpack的Gzip和服务端的Gzip

一般来水,Gzip压缩是服务器的活儿:服务器了解到我们这边有一个Gzip压缩的需求,它会启动自己的CPU去为我们完成这个任务。而压缩 文件这个过程本身是需要耗费时间的,可以理解为我们以服务器压缩的时间开销和CPU开销(以及浏览器解析解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。

既然存在这样的交换,那么就要求我们学会权衡。服务器的CPU性能不是无限的,如果存在大量的压缩需求,服务器也是扛不住的。服务器一旦因此慢下来了,用户还是要等,webpack中的gzip压缩操作的存在,事实上就是为了构建过程中去做一部分服务器的工作,为服务器分压。

因此,不管是webpack的Gzip还是服务器的Gzip,谁也不能替代谁,应该结合业务压力的实际强度情况,去做好其中的权衡。

实现

不是每个浏览器都支持Gzip,如何知道客户端是否支持Gzip呢,请求头中有个Accept-Encoding:gzip来标识对压缩的支持。客户端http请求头声明浏览器支持的压缩发方式,服务器配置启动压缩,压缩的文件类型,压缩方式。当客户端请求到服务端的时候,服务器解析请求头,如果客户端支持Gzip压缩,响应式对请求的资源进行压缩并返回客户端,浏览器按照自己的方式解析,在http响应头,我们可以看到content-encoding:gzip,这是指的服务端使用了Gzip的压缩方式

配置Gzip

1.安装compression插件

javascript 复制代码
npm install compression-webpack-plugin --s

2.配置vue.config.js

javascript 复制代码
const CompressionPlugin = require("compression-webpack-plugin"); // 引入
 
 
....
 config.plugin('compressionPlugin').use(new CompressionPlugin({
                algorithm: 'gzip',
                test: /\.js$|\.css$|\.html$/, // 匹配文件名
                threshold: 10240, // 对超过10k的数据压缩
                minRatio: 0.8,//压缩比
                deleteOriginalAssets: false // 不能删除源文件,不然报错"Uncaught SyntaxError: Unexpected token <"
            }))
....

3.配置nginx支持gzip的操作

javascript 复制代码
# 前端将文件打包成.gz文件,然后通过nginx的配置,让浏览器直接解析.gz文件,可以大大提升文件加载的速度。
http {
    # nginx开启Gzip:若没有找到.gz,会动态压缩,因此建议前端打包成.gz文件
    # 是否启用Gzip(on为启用,off为关闭)
    gzip  on;
    # 设置允许压缩的页面最小字节数,页面字节数从header头中的Content-Length中进行获取。默认值是0,不管页面多大都压缩。建议设置成大于1k的字节数,小于1k可能会越压越大。
    gzip_min_length 1k;
    # 获取多少内存用于缓存压缩结果,'4 16k'表示以16k*4为单位获得
    gzip_buffers 4 16k;
    # Gzip压缩比(1~9),越小压缩效果越差,但是越大处理越慢,所以一般取中间值;
    gzip_comp_level 5;
    # 对特定的MIME类型生效,其中'text/html'被系统强制启用(少啥类型就添加啥)
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
    # 识别http协议的版本,早起浏览器可能不支持Gzip自解压,用户会看到乱码
    gzip_http_version 1.1;
    # 启用应答头"Vary: Accept-Encoding"
    gzip_vary on;
    # 配置禁用 gzip 条件,支持正则,此处表示 ie6 及以下不启用 gzip(因为ie低版本不支持)
    gzip_disable "MSIE [1-6]\.";
}

gzip的优缺点

优点

减少文件大小,Gzip压缩比率在3-10倍左右,可以大大节省服务器的网络带宽。而在实际应用中,并不是对所有的文件都进行压缩,通常只是压缩静态文件。与此同时,减少文件大小有两个明显的好处:

  • 减少存储空间
  • 通过网络传输文件时,可以减少传输事件,以加快网站的打开速度
缺点
  • 需要nginx、服务端的支持,占用了一些服务器和客户端的CPU
  • 操作失误会造成网站无法访问
  • 蜘蛛无法进行爬行,造成收录不佳
  • 谷歌可以完美支持Gzipp压缩,百度支持的并不是很友好
相关推荐
落霞的思绪1 小时前
CSS复习
前端·css
咖啡の猫3 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲5 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路6 小时前
GeoTools 读取影像元数据
前端
ssshooter6 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry7 小时前
Jetpack Compose 中的状态
前端
dae bal8 小时前
关于RSA和AES加密
前端·vue.js
柳杉8 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog8 小时前
低端设备加载webp ANR
前端·算法