前端优化

165 阅读8分钟

一、静态资源-图片优化

1. 图片格式和应用场景

(1)JPG、JPEG
  • 非常适合:颜色丰富的照片、彩色图大焦点图、通栏banner图、结构不规则图形
  • 不适合:线条图形和文字、图标图形,因为它的压缩算法不太适合这些类型的图形;并且不支持透明。
(2)PNG
  • 非常适合:纯色、透明、线条绘图,图标;边缘清晰、有大块相同颜色区域;颜色数较少但需要半透明的。
  • 不适合:由于是无损压缩,彩色图像体积太大,所以不太适合。
(3)GIF
  • 非常适合:动图、动的loading图标。
  • 不适合:每个像素只有8比特,不适合存储彩色图片。

2. 用工具进行图片压缩

(1)压缩PNG
  • 跨平台,压缩比高,压缩png24非常好
  • 安装方法: npm install node-pngquant-native
(2)压缩JPG、JPEG
  • 跨平台,压缩比高
(3)压缩GIF
  • 使用方法: gifsicle --optimize=3 -o out.gif in.gif

3. 不同情况的图片处理

(1)图片尺寸随网络环境变化

不同网络环境(Wifi/4G/3G)下,加载不同尺寸和像素的图片,通过在图片 URL 后 缀加不同参数改变。

img13.360buyimg.com/n1/s100x100…

(2)图片使用统一占位符
  • 使用 SQIP • 基于 SVG 的图像占位符(SVG Quality Image Placeholders)
  • 安装:npm install sqip

4. 一般图片的替代方案

  • Web Font 代替图片,即字体图标
  • 使用 Data URI 代替图片,即base64图片, url-loader
module.exports = {
    chainWebpack: config => {
        // 10kb以内的图片会被打包成内联元素
        config.module
            .rule('images')
            .use('url-loader')
            .loader('url-loader')
            .tap(options => Object.assign(options, {limit: 10240}));
    }
};

5. 图片服务器处理图片

图片服务器自动化优化是可以在图片 URL 链接上增加不同特殊参数,服务器自动化生成。

6. 图片的懒加载

图片懒加载实现原理:

由于浏览器会自动对页面中的 img 标签的 src 属性发送请求并下载图片,可以通过 html5 自定义属性 data-xxx 先暂存

<img src="" alt="" data-src="./images/1.jpg">

vue-lazyload 插件为例

// 安装 
npm install vue-lazyload 
    
// main.js 注册
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 配置项
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png', // 图片加载失败时的占位图
  loading: 'dist/loading.gif', // 图片加载中时的占位图
  attempt: 1
})

// 通过 v-lazy 指令使用
<ul>  
    <li v-for="img in list">
        <img v-lazy="img.src" :key="img.src" >
    </li>
</ul>

二、静态资源-JS优化

1. js插件引入

  • 首选,npm包引入;没有这种方式时,使用script标签引入,引入地址使用.min.js
  • npm包引入,可以使用按需加载,首先按需加载引入
import debounce from "lodash/debounce"
import { addListener } from "resize-detector"

2. 路由文件使用异步加载和webpack魔法注释

const routes = [
  {
    path: "/user",
    hideInMenu: true,
    component: () =>
      import(/* webpackChunkName: "layout" */ "../layouts/UserLayout.vue"),
    children: [
      {
        path: "/user",
        redirect: "/user/login",
      },
      {
        path: "/user/login",
        name: "login",
        component: () =>
          import(/* webpackChunkName: "user" */ "../views/User/Login.vue"),
      },
      {
        path: "/user/register",
        name: "register",
        component: () =>
          import(/* webpackChunkName: "user" */ "../views/User/Register.vue"),
      },
    ],
  },
  {
    path: "/largescreen",
    name: "largescreen",
    meta: { title: "可视化大屏" },
    component: () =>
      import(
        /* webpackChunkName: "dashboard" */ "../views/Dashboard/VisualLargeScreen.vue"
      ),
  },
  {
    path: "/amap/basic",
    name: "amapBasic",
    meta: { title: "高德地图" },
    component: () =>
      import(
        /* webpackChunkName: "dashboard" */ "../views/Dashboard/amap/AmapIndex.vue"
      ),
  }
]

3. 查看打包后的js文件大小分布

npm run build -- --report   // webpack  npm i webpack-bundle-analyzer -D

4. 地区语言包太大

// moment.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    // Ignore all locale files of moment.js
    new webpack.IgnorePlugin(/^./locale$/, /moment$/),
  ],
};
// main.js
import "moment/locale/zh-cn"

5. 依赖包vendor太大,开启分离包

const webpack = require('webpack');
const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
  productionSourceMap: false,
  configureWebpack: config => {
    config.plugins.push(new webpack.IgnorePlugin(/^./locale$/, /moment$/))

    // 开启分离 js
    config.optimization = {
      runtimeChunk: 'single',
      splitChunks: {
        chunks: 'all',
        maxInitialRequests: Infinity,
        minSize: 20000,
        cacheGroups: {
          vendor: {
            test: /[\/]node_modules[\/]/,
            name (module) {
              // get the name. E.g. node_modules/packageName/not/this/part.js
              // or node_modules/packageName
              const packageName = module.context.match(/[\/]node_modules[\/](.*?)([\/]|$)/)[1]
              // npm package names are URL-safe, but some servers don't like @ symbols
              return `npm.${packageName.replace('@', '')}`
            }
          }
        }
      }
    }
  }
};

6. webpack splitChunks, 提取出来的通用 'echarts', 'moment', 'element-ui', 'xlsx'等

// ['echarts', 'moment', 'element-ui', 'xlsx', 'chunk-vendors', 'chunk-common', 'index']
module.exports = {
    pages: {
        index: {
            // ...
            // 在这个页面中包含的块,默认情况下会包含
            // 提取出来的通用 chunk 和 vendor chunk。
            chunks: ['echarts', 'moment', 'element-ui', 'xlsx', 'chunk-vendors', 'chunk-common', 'index']
        },
 
        chainWebpack: config => {
            if (process.env.NODE_ENV === 'production') {
                config.optimization.splitChunks({
                    chunks: 'async',
                    minSize: 1024 * 10, // 30000,
                    maxSize: 0,
                    minChunks: 1,
                    maxAsyncRequests: 6,
                    maxInitialRequests: 4,
                    automaticNameDelimiter: '~',
                    cacheGroups: {
                        // 链接:https://juejin.cn/post/6844904105555525640
                        echarts: {
                            name: 'echarts',
                            test: /[\/]node_modules[\/]echarts[\/]/,
                            minSize: 0,
                            minChunks: 1,
                            reuseExistingChunk: true,
                            chunks: 'all'
                        },
                        moment: {
                            name: 'moment',
                            test: /[\/]node_modules[\/]moment[\/]/,
                            minSize: 0,
                            minChunks: 1,
                            reuseExistingChunk: true,
                            chunks: 'all'
                        },
                        'element-ui': {
                            name: 'element-ui',
                            test: /[\/]node_modules[\/]element-ui[\/]/,
                            minSize: 0,
                            minChunks: 1,
                            reuseExistingChunk: true,
                            chunks: 'all'
                        },
                        xlsx: {
                            name: 'xlsx',
                            test: /[\/]node_modules[\/]xlsx[\/]/,
                            minSize: 0,
                            minChunks: 1,
                            reuseExistingChunk: true,
                            chunks: 'all'
                        },
 
                        vendors: {
                            name: 'chunk-vendors',
                            test: /[\/]node_modules[\/]/,
                            priority: -10,
                            chunks: 'initial'
                        },
                        common: {
                            name: 'chunk-common',
                            minChunks: 2,
                            priority: -20,
                            chunks: 'initial',
                            reuseExistingChunk: true
                        }
                    }
                });
            }
        }
    }
}

三、开启和配置GZIP压缩

1. 作用

  • 对文本进行压缩(HTML/CSS/JS)
  • 对非文本不压缩(jpg/png/gif)
  • 压缩比约50% - 70%

2. 服务端Nginx配置:

// nginx.conf

http {
	gzip on;
	gzip_static on;
	gzip_min_length 1k;
	gzip_buffers 4 32k;
	gzip_http_version 1.1;
	gzip_comp_level 2;
	gzip_types text/plain application/x-javascript text/css application/xml;
	gzip_vary on;
	gzip_disable "MSIE [1-6].";

    server {
    
    }
}

3. 检查生效:

  • Response Header查看是否有Content-Encoding: gzip,  代表服务端已开启gzip

4. 配置webpack压缩插件compression-webpack-plugin

npm install compression-webpack-plugin@6.1.0 --save-dev
// vue.config.js
const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
    productionSourceMap: false,
    configureWebpack: config => {
        config.plugins.push(new CompressionWebpackPlugin({
            test: /\.(js|css|json|ico|svg)$/, // 匹配文件名
            algorithm: 'gzip',
            minRatio: 0.8,
            threshold: 5120, // 对超过5k的数据压缩
            deleteOriginalAssets: false // 是否删除原资源
        }));
    }
}

四、优化打包速度

1. 使用speed-measure-webpack-plugin插件测量打包各环节耗费时间

npm install speed-measure-webpack-plugin --save-dev
// vue.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

module.exports = {
  // 这里无法使用链式写法chainWebpack,会报错
  configureWebpack: smp.wrap({
    // ... webpack config goes here ...
  }
}

2. CDN分包

cdn分包可以让我们在打包过程中将特定包分离出来,从而提升打包速度。

npm install html-webpack-externals-plugin --save-dev

但是如果Webpack的版本过高,安装可能出现版本冲突,可以先用 --force命令强制安装,等出现冲突问题再卸载解决。

3. 多进程构建

耗时较长的模块,同时开启多个 nodejs 进程进行构建,可以有效地提升打包的速度。可以采取的一些方式有:

thread-loader
HappyPack(已经不维护)
parallel-webpack

npm install thread-loader --save-dev
// webpack.config.js
module: {
      strictExportPresence: true,
      rules: [
        // 多进程构建
        {
          test: /.js$/,
          include: path.resolve('src'),
          use: [
            "thread-loader",
            // 耗时的 loader (例如 babel-loader)
          ],
        },

4. 缩小构建范围

构建过程是默认全局查找,这非常的耗时。通过手动配置rules中的文件查找范围,可以减少打包的范围,从而提升打包的速度。

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.js$/,
        use: ['babel-loader'],
        exclude: /node_modules/,
      },
    ],
  },
};

可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件,用include去命中需要被babel-loader处理的文件,exclude去排除不需要被处理的文件,从而缩小了 Loader 处理文件的范围。

5. 使用缓存

通过使用缓存机制能有效提升二次打包速度。目前webpack5 内置了 cache 模块,缓存生成的 webpack 模块和 chunk,来改善构建速度。它在开发环境下会默认设置为 type: 'memory' 而在生产环境中被禁用。可以通过设置 cache: { type: 'filesystem' } 来开放更多配置项。

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',
  },
};

第一次打包后,就会在node_modules文件夹下面生成一个cache文件夹,从而第二次打包速度显著提升。

五、Web Worker优化长任务

由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况

查看页面的长任务:

打开控制台,选择 Performance 工具,点击 Start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务(长任务:执行时间超过50ms的任务)

web worker.png 测试实验:

如果直接把下面这段代码直接丢到主线程中,计算过程中页面一直处于卡死状态,无法操作

let sum = 0;
for (let i = 0; i < 200000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random()
    }
  }
复制代码

使用 Web Worker 执行上述代码时,计算过程中页面正常可操作、无卡顿

// worker.js
onmessage = function (e) {
  // onmessage获取传入的初始值
  let sum = e.data;
  for (let i = 0; i < 200000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random()
    }
  }
  // 将计算的结果传递出去
  postMessage(sum);
}

Web Worker 的通信时长

并不是执行时间超过 50ms 的任务,就可以使用 Web Worker,还要先考虑通信时长的问题

假如一个运算执行时长为 100ms,但是通信时长为 300ms, 用了 Web Worker可能会更慢

比如新建一个 web worker, 浏览器会加载对应的 worker.js 资源,下图中的 Time 是这个资源的通信时长(也叫加载时长)

web worker02.png

当任务的运算时长 - 通信时长 > 50ms,推荐使用Web Worker

六、requestAnimationFrame优化动画

requestAnimationFrame 是浏览器专门为动画提供的 API,它的刷新频率与显示器的频率保持一致,使用该 api 可以解决用 setTimeout/setInterval 制作动画卡顿的情况

setTimeout/setInterval、requestAnimationFrame 三者的区别:

1)引擎层面

setTimeout/setInterval 属于 JS引擎,requestAnimationFrame 属于 GUI引擎

JS引擎与GUI引擎是互斥的,也就是说 GUI 引擎在渲染时会阻塞 JS 引擎的计算

2)时间是否准确

requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout/setInterval 是宏任务,根据事件轮询机制,其他任务会阻塞或延迟js任务的执行,会出现定时器不准的情况

3)性能层面

当页面被隐藏或最小化时,setTimeout/setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停

let box1 = document.getElementById('box1');
let btn1 = document.getElementById('btn1');
let point1 = document.getElementById('point1');
let handel1 = 0;
let $width1 = 0;
let left1 = -10;
function setWidth1(params) {
    box1.style.width = $width1 + 'px';
    point1.style.left = $width1 + left1 + 'px';
    handel1 = window.requestAnimationFrame(setWidth1);
    $width1 <= 396 ? ($width1 += 2) : (btn1.style.display = 'none');
    document.getElementById('txt1').innerText = Math.ceil(($width1 / 400) * 100);
}

setWidth1();
btn1.addEventListener(
    'click',
    function () {
      if (handel1) {
        window.cancelAnimationFrame(handel1);
        handel1 = 0;
        btn1.innerText = '播放';
      } else {
        setWidth1();
        btn1.innerText = '暂停';
      }
    },
    false
);

七、js加载方式

1)正常模式

<script src="index.js"></script>

这种情况下 JS 会阻塞 dom 渲染,浏览器必须等待 index.js 加载和执行完成后才能去做其它事情

2)async 模式

<script async src="index.js"></script>

async 模式下,它的加载是异步的,JS 不会阻塞 DOM 的渲染,async 加载是无顺序的,当它加载结束,JS 会立即执行

使用场景:若该 JS 资源与 DOM 元素没有依赖关系,也不会产生其他资源所需要的数据时,可以使用async 模式,比如埋点统计

3)defer 模式

<script defer src="index.js"></script>

defer 模式下,JS 的加载也是异步的,defer 资源会在 DOMContentLoaded 执行之前,并且 defer 是有顺序的加载

如果有多个设置了 defer 的 script 标签存在,则会按照引入的前后顺序执行,即便是后面的 script 资源先返回

所以 defer 可以用来控制 JS 文件的执行顺序,比如 element-ui.js 和 vue.js,因为 element-ui.js 依赖于 vue,所以必须先引入 vue.js,再引入 element-ui.js

<script defer src="vue.js"></script>
<script defer src="element-ui.js"></script>

defer 使用场景:一般情况下都可以使用 defer,特别是需要控制资源加载顺序时

4)module 模式

<script type="module">import { a } from './a.js'</script>

在主流的现代浏览器中,script 标签的属性可以加上 type="module",浏览器会对其内部的 import 引用发起 HTTP 请求,获取模块内容。这时 script 的行为会像是 defer 一样,在后台下载,并且等待 DOM 解析

Vite 就是利用浏览器支持原生的 es module 模块,开发时跳过打包的过程,提升编译效率

5) preload

<link rel="preload" as="script" href="index.js">

link 标签的 preload 属性:用于提前加载一些需要的依赖,这些资源会优先加载(如下图红框)

vue2 项目打包生成的 index.html 文件,会自动给首页所需要的资源,全部添加 preload,实现关键资源的提前加载

preload 特点:

1)preload 加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件;

2)preload 加载的 JS 脚本其加载和执行的过程是分离的,即 preload 会预加载相应的脚本代码,待到需要时自行调用;

6)prefetch

<link rel="prefetch" as="script" href="index.js">
复制代码

prefetch 是利用浏览器的空闲时间,加载页面将来可能用到的资源的一种机制;通常可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度

prefetch 特点:

1)pretch 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少5分钟(无论资源是否可以缓存)

2)当页面跳转时,未完成的 prefetch 请求不会被中断

加载方式总结

async、defer 是 script 标签的专属属性,对于网页中的其他资源,可以通过 link 的 preload、prefetch 属性来预加载

如今现代框架已经将 preload、prefetch 添加到打包流程中了,通过灵活的配置,去使用这些预加载功能,同时我们也可以审时度势地向 script 标签添加 async、defer 属性去处理资源,这样可以显著提升性能

八、组件懒加载

当用户打开 home 页时,会一次性加载该页面所有的资源,我们期望的是用户触发按钮后,再加载该弹框组件的资源

这种场景下,就很适合用懒加载的方式引入

弹框组件懒加载:

<script>
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo');
export default {
  name: 'homeView',
  components: {
    dialogInfo
  }
}
</script>

重新打包后,home.js 和 about.js 中没有了弹框组件的代码,该组件被独立打包成 dialogInfo.js,当用户点击按钮时,才会去加载 dialogInfo.js 和 dialogInfo.cs

组件懒加载的使用场景

有时资源拆分的过细也不好,可能会造成浏览器 http 请求的增多

总结出三种适合组件懒加载的场景:

1)该页面的 JS 文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页)

2)该组件不是一进入页面就展示,需要一定条件下才触发(比如弹框组件)

3)该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 JS 文件大小(比如表格组件、图形组件等)