最新前端性能优化方案(支持 Vite 和 Webpack)

前言

前端性能优化可以大致可以从三个方面入手:打包构建性能、网络传输性能、运行性能。这三个方面涵盖了我们从打包部署到用户使用的全过程。本文将介绍一些具体的实施办法。

打包构建性能

压缩代码

代码压缩可以减少页面的加载时间、减少对网络带宽的占用、增强代码安全性。

Webpack 提供了许多压缩代码的插件,下面介绍其中两个常用的插件:

  • UglifyJSPlugin

    UglifyJSPlugin 是一个可以将 JS 代码压缩至极致的插件。可以通过以下方式来安装和配置:

    安装:

    npm i uglifyjs-webpack-plugin -D
    

    引入:

    // webpack.config.js
    const UglifyJsPlugin = require("uglifyjs-webpack-plugin")
    
    module.exports = {
      plugins: [new UglifyJsPlugin()],
    }
    
  • OptimizeCSSAssetsPlugin

    OptimizeCSSAssetsPlugin 是一个可以压缩 CSS 代码的插件。可以通过以下方式来安装和配置:

    安装:

    npm i optimize-css-assets-webpack-plugin -D
    

    引入:

    // webpack.config.js
    const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
    
    module.exports = {
      optimization: {
        minimizer: [new OptimizeCSSAssetsPlugin({})],
      },
    }
    

通过使用 Webpack 提供的 UglifyJSPlugin 和 OptimizeCSSAssetsPlugin 插件,可以轻松地压缩 JavaScript 和 CSS 代码,从而提高页面加载速度和性能。

Vite 压缩代码:

Vite 中默认开启了代码压缩,下面是 Vite 的默认配置,一般情况下不需要修改。

// vite.config.js
import { defineConfig } from "vite"
export default defineConfig({
  build: {
    minify: "esbuild", // boolean | 'terser' | 'esbuild'
  },
})

如果需要,可以通过修改 minify 选项更改压缩方式。默认为 Esbuild,它比 terser 快 20-40 倍,压缩率只差 1%-2%。

压缩打包体积

压缩打包体积可以使用 GZIP,能大幅度减小打包后的文件大小。

WebPack 开启 GZIP

使用 compression-webpack-plugin 插件可以在构建过程中对文件进行 gzip 压缩,并生成对应的 .gz 文件。

安装:

npm i compression-webpack-plugin -D

引入:

// webpack.config.js
const CompressionPlugin = require("compression-webpack-plugin")

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: "gzip",
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8,
    }),
  ],
}

在上面的配置中使用了 compression-webpack-plugin 插件对所有的 .js、.css、.html 和 .svg 文件进行 gzip 压缩,并设置了压缩的阈值和比率。

Vite 开启 GZIP

在 Vite 中开启 gzip,可以使用 vite-plugin-compression 插件来实现。这个插件可以在构建过程中对文件进行 gzip 压缩,并生成对应的 .gz 文件。

安装:

npm i vite-plugin-compression -D

引入:

// vite.config.js
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import viteCompression from "vite-plugin-compression"

export default defineConfig({
  plugins: [
    vue(),
    viteCompression({
      threshold: 10240, // the unit is Bytes
    }),
  ],
})

在这个配置中,我们使用 vite-plugin-compression 插件对所有大于 10KB 的文件进行 gzip 压缩,并设置了压缩的阈值。在构建过程中,插件会自动在输出目录下生成对应的 .gz 文件。然后在 Web 服务器的配置文件中开启 gzip,当浏览器请求文件时,服务器会将对应的 gzip 文件返回给浏览器。

更多配置,请参考 👉 vbenjs/vite-plugin-compression: Use gzip or brotli to compress resources

在服务器中开启 GZIP

开启 gzip 需要同时确保 Web 服务器已经开启了对应的 gzip 支持。

  • 以 nginx 为例:

    server {
      gzip on;
      gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
      gzip_min_length 10240;
      gzip_comp_level 6;
    }
    
  • 以 nodejs 为例:

    npm i compression
    
    // app.js
    const compression = require("compression")
    
    app.use(compression())
    

压缩图片

WebPack 压缩图片

在 Webpack 中,可以通过 image-webpack-loader 对图片进行压缩和优化处理。

安装:

npm i image-webpack-loader -D

引入:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif)$/i,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "[name].[ext]",
              outputPath: "images/",
              publicPath: "images/",
            },
          },
          {
            loader: "image-webpack-loader",
            options: {
              mozjpeg: {
                quality: 80,
              },
              pngquant: {
                quality: [0.65, 0.9],
                speed: 4,
              },
              gifsicle: {
                interlaced: false,
              },
              webp: {
                quality: 75,
              },
            },
          },
        ],
      },
    ],
  },
}

在这个配置中,我们使用 file-loader 处理图片文件,然后使用 image-webpack-loader 插件对图片进行压缩和优化处理,其中 options 可以设置不同的压缩算法和参数。

Vite 压缩图片

在 Vite 中常用的图片压缩插件有 imageminsquoosh,下面以 squoosh 为例:

  1. 安装 vite-plugin-squoosh

    npm install vite-plugin-squoosh -D
    
  2. 配置 vite-plugin-squoosh

    // vite.config.js
    const { defineConfig } = require("vite")
    const vue = require("@vitejs/plugin-vue")
    const squooshPlugin = require("vite-plugin-squoosh")
    
    module.exports = defineConfig({
      plugins: [
        vue(),
        squooshPlugin({
          // Specify codec options.
          codecs: {
            mozjpeg: { quality: 30, smoothing: 1 },
            webp: { quality: 25 },
            avif: { cqLevel: 20, sharpness: 1 },
            jxl: { quality: 30 },
            wp2: { quality: 40 },
            oxipng: { level: 3 },
          },
          // Do not encode .wp2 and .webp files.
          exclude: /.(wp2|webp)$/,
          // Encode png to webp.
          encodeTo: [{ from: /.png$/, to: "webp" }],
        }),
      ],
    })
    

更多配置请参考 👉 vite-plugin-squoosh

网络传输性能

大量的 HTTP 请求会给服务器和浏览器造成一定的压力,导致前端获取响应的时间增加,从而造成页面加载时间过长。优化网络传输性能可以从以下几个方面入手:

图片使用 LazyLoad 懒加载

懒加载也称为延迟加载。基本思想是,在页面加载时只加载当前页面可视区域的图片,而其它图片等用户滚动到可视区域页面时再进行加载。

Vue 中实现图片懒加载

在 Vue 中实现图片懒加载,可以使用 vue-lazyload 插件

  1. 安装 vue-lazyload

    npm i vue-lazyload
    
  2. 引入 vue-lazyload

    // main.js
    import Vue from "vue"
    import VueLazyload from "vue-lazyload"
    
    Vue.use(VueLazyload)
    
  3. 在需要懒加载的图片上使用 v-lazy 指令

    <template>
      <img v-lazy="imgUrl" alt="图片" />
    </template>
    

更多配置点击这里 👉 vue-lazyload

React 中实现图片懒加载

在 React 中实现图片懒加载,可以借助 react-lazyload

  1. 安装 react-lazyload

    npm i react-lazyload
    
  2. 在组件中引入 react-lazyload

    import React from "react"
    import LazyLoad from "react-lazyload"
    
    function MyComponent() {
      return (
        <div>
          <LazyLoad>
            <img src='path/to/image' alt='image' />
          </LazyLoad>
        </div>
      )
    }
    

更多配置点击这里 👉 react-lazyload

原生 JS 实现图片懒加载

原生 JS 实现图片懒加载可以使用 Element.getBoundingClientRect() 方法,该方法会返回元素的大小及其相对于视口的位置。然后判断图片是否出现在页面上。

<img class="lazyload" data-src="https://picsum.photos/200/300" />
<img class="lazyload" data-src="https://picsum.photos/200/300" />
<img class="lazyload" data-src="https://picsum.photos/200/300" />
// 获取需要懒加载的图片元素
const lazyloadImages = document.querySelectorAll(".lazyload")
// 获取视口高度和宽度
const windowHeight = window.innerHeight
const windowWidth = window.innerWidth

// 判断一个元素是否在视口内
function isInViewport(element) {
  const rect = element.getBoundingClientRect()
  return rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowHeight && rect.right <= windowWidth
}

// 监听窗口的滚动事件
window.addEventListener("scroll", function () {
  lazyloadImages.forEach(img => {
    if (isInViewport(img)) {
      img.src = img.dataset.src
      img.classList.remove("lazyload")
    }
  })
})

使用 CSS3 代替简单图片

可以利用 CSS3 代替一些简单的图片,这样就可以减少一些图片请求,提高页面加载速度。

使用 Data URI

将小图片转为 Data URI 格式,直接嵌入 CSS 或 HTML 中,可以避免请求。但是需要注意,Data URI 格式的图片会增加页面大小,需要根据实际情况进行权衡和选择。下面介绍在 Webpack 和 Vite 中将小图片转换为 base64 编码的 Data URI 格式的方法。

  • Webpack 中将小图片转换为 base64

    在 Webpack 中,可以使用 url-loader 或者 file-loader 将小图片转换成 base64 编码的 Data URI 格式。这两个 loader 都是用于处理文件的,可以将文件转换成模块,以便在代码中引用。

    其中,url-loader 和 file-loader 的主要区别在于处理方式不同。url-loader 在处理图片时,会先判断图片大小是否超过指定的限制,如果超过了限制,则使用 file-loader 进行处理;如果没有超过限制,则将图片转换成 base64 编码的 Data URI 格式,并嵌入到代码中,以减少 HTTP 请求次数。

    // webpack.config.js
    module.exports = {
      module: {
        rules: [
          {
            test: /\.(png|jpg|gif)$/,
            use: [
              {
                loader: "url-loader",
                options: {
                  limit: 8192, // 小于 8KB 的图片会被转换成 base64 编码的 Data URI 格式
                  fallback: "file-loader", // 超过 8KB 的图片使用 file-loader 进行处理
                },
              },
            ],
          },
        ],
      },
    }
    
  • Vite 中将小图片转换为 base64

    在 Vite 中,可以通过配置 assetsInlineLimit 选项来指定大小限制,小于指定大小的图片将被转换成 base64 编码的 Data URI 格式。

    下面是在 Vite 中配置 assetsInlineLimit 的示例:

    // vite.config.js
    module.exports = {
      build: {
        assetsInlineLimit: 8192, // 小于 8KB 的图片会被转换成 base64 编码的 Data URI 格式
      },
    }
    

使用雪碧图或字体图标代替图标图片

  • 将多个小图标合并成一张图片,通过 CSS 背景定位来显示不同的图片,减少请求次数。

  • 或者使用字体图标来代替图标图片,减少请求次数。

合并文件

将多个 CSS 文件或 JavaScript 文件合并成一个文件,减少请求次数。

使用 HTTP/2

HTTP/2 是一种新的协议,相比 HTTP/1.1,可以更快地传输数据。 HTTP/1.1 的 Headers 采用的是文本格式,并且每一次请求都会带上一些完全相同的数据,而 HTTP/2 采用的是二进制编码,并且对 Headers 进行了 HPack 压缩,进而提升了传输效率。不仅如此,HTTP/2 还可以同时发送多个请求和响应,因此可以通过合并和压缩资源,减少请求的数量,提高页面加载速度。

HTTP/2 是向下兼容的,当浏览器不支持的时候会自动切换到 HTTP/1.1。但需要在服务器端和客户端同时配置才能生效。

可以通过以下步骤来升级到 HTTP/2:

  1. 使用 HTTPS:HTTP/2 只支持加密连接,因此需要使用 HTTPS 来使用 HTTP/2。

  2. 在服务器端启用 HTTP/2:需要在服务器端启用 HTTP/2,以便前端可以使用该协议。常见的 Web 服务器,如 Nginx 和 Apache,都支持 HTTP/2。

Nginx 启用 HTTP/2 请参考这里 👉 Supporting HTTP/2 for Google Chrome Users | NGINX

另外,关于 HTTP 版本之间的区别,可以参考 👉 了解 HTTP 的前世今生

异步加载 JS 和 CSS

使用异步加载可以提高页面响应速度,具体来说,可以采取以下措施:

  • 将 JavaScript 脚本放在页面底部,或者使用 defer 或 async 属性延迟 JavaScript 加载和执行。

  • 将 CSS 样式通过 link 标签异步加载,也可以使用 JavaScript 动态加载样式表。

使用 CDN

服务器的位置是固定的,负载也是有限的。通常访客区域距离服务器越远,打开网站速度越慢。如果在高峰时间段,网站访问量很大,服务器无法负载,也会导致访问速度下降。

因此,我们可以将某些资源放到 CDN 上,这样就可以减少对服务器的 HTTP 请求。从而提高页面加载速度。


运行性能

避免重绘(Repaint)和重排(Reflow)

重绘和重排都会导致页面或部分页面重新渲染,所以我们应当尽量避免触发这两个浏览器事件。

重绘(Repaint) 就是浏览器使用新的样式重新渲染一个元素。改变元素的以下样式通常会触发重绘,应当尽量避免:

  • background

  • border

  • border-radius

  • box-shadow

  • color

  • visibility

  • outline

如果需要改变元素的某些 CSS 属性,尽量一次性改变,减少触发重绘的次数。

重排(Reflow) 就是浏览器重新渲染部分或全部页面,通常是当元素的尺寸发生改变或者浏览器的一些行为影响到页面布局而触发的,比如

  • clientWidthclientHeight 等窗口属性改变

  • box-sizingwidthheightborderpadding 等元素尺寸改变

  • font-sizeline-height 等元素字体大小改变

  • marginfloatflex-direction 等元素位置改变

如果需要改变元素位置或尺寸,可以使用以下属性避免重排:

  • position: fixed | absolute 使元素脱离文档流。

  • transform 属性不会触发避免重排。

使用节流防抖

在用户浏览网页或者在网页上进行一些操作时,我们常常需要监听一些事件去完成相应的功能。比如鼠标点击、键盘输入、滚轮滚动等一些其它事件。

在处理这些事件时,我们常常会发现,如果一个事件发生的频率过高,相应的代码执行的频率也会越高。

所以,我们并不需要代码如此高频率的执行,这在一定程度上对系统资源造成了浪费,程序的性能也会因此变得很差。因此,我们需要使用节流防抖,对代码的执行频率进行一些限制。

基本的节流函数

/**
 * @description 节流函数
 * @param {Function} func 需要节流的函数
 * @param {Number} wait 节流的时间
 * @returns {Function} 返回节流后的函数
 */
function throttle(func, wait) {
  let timer = null
  return function () {
    if (timer) return
    timer = setTimeout(() => {
      func.apply(this, arguments)
      timer = null
    }, wait)
  }
}

基本的防抖函数

/**
 * @description 防抖函数
 * @param {Function} func 需要防抖的函数
 * @param {Number} wait 防抖的时间
 * @returns {Function} 返回防抖后的函数
 */
function debounce(func, wait) {
  let timeout
  return function () {
    timeout && clearTimeout(timeout)
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, wait)
  }
}

使用示例

// 节流
input.addEventListener("input", throttle(printInputText, 500))

// 防抖
input.addEventListener("input", debounce(printInputText, 500))

更多有关节流防抖的使用,可以参考 👉 手写实现节流防抖

使用服务端渲染(SSR)

SSR(Server-Side Rendering,服务端渲染)是指将 Web 页面的生成过程从浏览器端转移到服务器端完成的一种技术。

在传统的 SPA(Single Page Application,单页应用)中,浏览器需要先下载 HTML、CSS、JavaScript 文件,并在客户端执行 JavaScript 代码,才能生成页面内容。这样的过程需要加载大量的 JavaScript 代码,会导致首屏渲染时间较长,影响用户体验。而通过 SSR 技术,服务器可以将页面的 HTML、CSS 和 JavaScript 代码预先生成好,并将渲染好的 HTML 代码直接返回给浏览器,从而加快页面加载速度,提高用户体验。

SSR 在性能提升方面有以下优点:

  • 提高页面加载速度:SSR 可以将渲染页面的工作从浏览器端转移到服务器端,减少浏览器的工作量,从而加快页面加载速度。

  • 提高首屏渲染速度:SSR 可以将页面的渲染过程提前到服务器端完成,使得页面在浏览器端显示的速度更快,从而提高用户体验。

  • 更好的可访问性:通过 SSR 技术,我们可以生成更符合 Web 标准的 HTML 页面,从而使得页面具有更好的可访问性。

有关 SSR 的更多介绍,可以参考 五分钟了解 SPA 与 SSR

使用 Web Workers

使用 Web Workers 可以将一些任务放在后台线程中执行,以避免阻塞主线程。主线程通常用于处理用户界面的更新和响应用户的操作,如果在主线程中执行耗时的任务,会导致用户界面出现卡顿或失去响应。使用 Web Workers 可以将这些耗时的任务放在后台线程中执行,从而避免阻塞主线程。Web Workers 可以与主线程进行通信,使得后台线程可以向主线程发送消息,而主线程也可以向后台线程发送消息。这种通信可以通过 postMessage 方法实现。

Web Workers 的使用有一定的限制,例如它们不能访问 DOM 和一些浏览器 API。但是,如果应用程序需要处理大量数据或进行复杂的计算,使用 Web Workers 可以提高应用程序的性能和响应速度。

以下是一个使用 Web Workers 的实际案例和代码示例:

假设我们有一个应用程序需要处理大量的数据,例如计算数组中所有元素的平均值。由于数据量很大,如果在主线程中执行这个任务,会导致页面出现卡顿或失去响应。因此,我们可以使用 Web Workers 将这个任务放在后台线程中执行,从而避免阻塞主线程。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Web Workers Example</title>
  </head>
  <body>
    <p>计算数组中所有元素的平均值:</p>
    <p id="result"></p>
    <script>
      // 创建 Web Worker
      const worker = new Worker("worker.js")
      // 发送消息给 Web Worker
      worker.postMessage([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
      // 监听 Web Worker 的消息
      worker.onmessage = function (event) {
        // 将计算结果显示在页面上
        document.getElementById("result").textContent = event.data
      }
    </script>
  </body>
</html>
// worker.js
// 监听主线程的消息
onmessage = function (event) {
  // 计算数组中所有元素的平均值
  const sum = event.data.reduce((a, b) => a + b, 0)
  const average = sum / event.data.length
  // 发送计算结果给主线程
  postMessage(average)
}

参考资料:

特别感谢:

  • ChatGPT
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容