前言
前端性能优化可以大致可以从三个方面入手:打包构建性能、网络传输性能、运行性能。这三个方面涵盖了我们从打包部署到用户使用的全过程。本文将介绍一些具体的实施办法。
打包构建性能
压缩代码
代码压缩可以减少页面的加载时间、减少对网络带宽的占用、增强代码安全性。
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 中常用的图片压缩插件有 imagemin 和 squoosh,下面以 squoosh
为例:
-
安装
vite-plugin-squoosh
npm install vite-plugin-squoosh -D
-
配置
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
插件。
-
安装
vue-lazyload
npm i vue-lazyload
-
引入
vue-lazyload
// main.js import Vue from "vue" import VueLazyload from "vue-lazyload" Vue.use(VueLazyload)
-
在需要懒加载的图片上使用
v-lazy
指令<template> <img v-lazy="imgUrl" alt="图片" /> </template>
更多配置点击这里 👉 vue-lazyload。
React 中实现图片懒加载
在 React 中实现图片懒加载,可以借助 react-lazyload
。
-
安装
react-lazyload
npm i react-lazyload
-
在组件中引入 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:
使用 HTTPS:HTTP/2 只支持加密连接,因此需要使用 HTTPS 来使用 HTTP/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) 就是浏览器重新渲染部分或全部页面,通常是当元素的尺寸发生改变或者浏览器的一些行为影响到页面布局而触发的,比如
clientWidth
、clientHeight
等窗口属性改变box-sizing
、width
、height
、border
、padding
等元素尺寸改变font-size
、line-height
等元素字体大小改变margin
、float
、flex-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