读懂前端「性能优化」

背景

随着互联网的发展,用户对网页加载速度和交互体验的要求越来越高,前端性能优化是提高网页性能的关键,性能优化是前端开发避不开的话题,一个完美的网站必定是能够给用户提供更优的体验。本文将介绍一些常见的前端性能优化技巧,帮助开发者提高网页加载速度,提升用户体验。

一、性能优化的本质

前端性能优化的本质在于提供更快速、更可靠、更高效的用户体验。优化网站性能不仅仅是为了让网站加载更快,更是为了提高用户满意度、降低跳出率、提升转化率,并最终实现业务目标。

二、基于chrome浏览器分析的性能优化指标

2.1 以用户为中心

  • First Paint 首次绘制(FP)这个指标用于记录页面第一次绘制像素的时间,如显示页面背景色。
  • First contentful paint 首次内容绘制 (FCP)LCP是指页面开始加载到最大文本块内容或图片显示在页面中的时间。如果 FP 及 FCP 两指标在 2 秒内完成的话我们的页面就算体验优秀。
  • Largest contentful paint 最大内容绘制 (LCP)用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。官方推荐的时间区间,在 2.5 秒内表示体验优秀
  • First input delay 首次输入延迟 (FID)首次输入延迟,FID(First Input Delay),记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟。
  • Time to Interactive 可交互时间 (TTI)首次可交互时间,TTI(Time to Interactive)

2.2 三大核心指标(Core Web Vitals)

“网页指标”是 Google 的一项计划,旨在针对网页质量信号提供统一指南,这些信号对于提供出色的网页用户体验至关重要。它的目标是简化各种可用的性能测量工具,并帮助网站所有者专注于最重要的指标,即核心网页指标

Google 在20年五月提出了网站用户体验的三大核心指标:加载、交互和渲染稳定性

2.2.1 LCP 加载速度指标

根据 W3C Web 性能工作组中的讨论和 Google 的研究,我们发现,衡量页面主要内容的加载时间的更准确方法是查看最大元素的呈现时间。

那么哪些元素可以被定义为最大元素呢?

  • <img> 元素
  • <svg> 元素内的 <image> 元素
  • <video> 元素
  • CSS background url()加载的图片
  • 包含文本节点或其他内嵌级文本元素子元素的块级元素

在 JavaScript 中衡量 LCP,如下示例用 largest-contentful-paint 条目并将其记录到控制台的 PerformanceObserver

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

项目中如何优化LCP

  • 将该样式表内嵌到 HTML 中,以避免产生额外的网络请求
  • 移除未使用的CSS:使用 Chrome 开发者工具查找未使用的 CSS 规则,这些规则可能会被移除(或延迟)。
  • 推迟非关键CSS:将样式表拆分为用于初始网页加载的样式,然后拆分为可延迟加载的样式。
  • 缩减CSS大小:对于关键样式,请务必尽可能减小其传输大小
  • 延迟或内嵌阻止呈现的 ,使用 async 或 defer 属性将网页上的所有脚本设为异步。使用同步脚本几乎总是会影响性能
  • 使用服务器端渲染「目前已重构的前端各类官网开发方式」

线上测量工具

使用工具

2.2.2 FID「用户操作页面的交互指标」

衡量的是从用户首次与网页互动(即,点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器实际能够开始处理事件处理脚本以响应相应互动的时间。

看用户交互事件触发到页面响应中间耗时多少,如果其中有长任务发生的话那么势必会造成响应时间变长。推荐响应用户交互在 100ms 以内

线上测量工具

用first-input 条目并将其记录到控制台的 PerformanceObserver

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    const delay = entry.processingStart - entry.startTime;
    console.log('FID candidate:', delay, entry);
  }
}).observe({type: 'first-input', buffered: true});

项目中如何优化FID

  • 减少首屏请求数量和请求文件大小
  • 减少初始化脚本的的执行时间
  • 最小化主线程工作
  • 防止阻断性逻辑执行,回流重绘渲染

2.2.3 CLS 「渲染稳定性

CLS 衡量的是网页生命周期内发生的每次意外布局偏移的最大布局偏移得分。只要可见元素的位置从一个渲染的帧更改为下一个渲染帧,就会发生布局偏移

为了计算布局偏移得分,浏览器会考虑视口大小,以及视口中不稳定元素在两个渲染帧之间的移动。布局偏移分数是该移动的两种度量的乘积:影响分数和距离分数。

layout 布局分数 = impact fraction * distance fraction

在本例中,红色虚线矩形表示元素在两个帧上的可见区域,在本例中占总视口的 75%,因此其影响比例为 0.75。最大的视口尺寸是高度,不稳定元素移动了视口高度的 25%,因此距离比例为 0.25

如果影响比例为 0.75,距离比例为 0.25,则布局偏移得分为 0.75 * 0.25 = 0.1875

PerformanceObserver 以将 layout-shift 条目记录到控制台

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Layout shift:', entry);
  }
}).observe({type: 'layout-shift', buffered: true});

线上测量工具

使用工具

通常,为了响应用户互动(例如点击或点按链接、按下按钮或在搜索框中输入)而发生的布局偏移是可以接受的,只要发生的位置距离互动足够接近且用户能够清楚了解两者之间的关系即可。

在项目初始化时必要的位移轨迹,最好使用动画来过渡,一般使用CSS transform 属性可以为元素添加动画效果,而不会触发布局偏移

2.3 性能工具:工欲善其事,必先利其器

Google开发的 所有工具 都支持Core Web Vitals的测量。工具如下:

总结:

  • 首先使用Lighthouse,在本地进行测量,根据报告给出的一些建议进行优化;
  • 然后使用PageSpeed Insights去看下线上的性能情况;
  • 使用Chrome User Experience Report API去捞取线上一周内的数据;
  • 发现数据有异常,我们可以使用DevTools工具进行具体代码定位分析;
  • 使用Search Console’s Core Web Vitals report查看网站功能整体情况;
  • 使用Web Vitals扩展方便的看页面核心指标情况;

三、前端性能优化具体方向

3.1 http性能优化

3.1.1 HTTP 1.1

HTTP/1.1中大多数的网站性能优化技术都是减少向服务器发起的HTTP请求数。浏览器可以同时建立有限个TCP连接,而通过这些连接下载资源是一个线性的流程:一个资源的请求响应返回后,下一个请求才能发送。这被称为线头阻塞。

在HTTP/1.1中,Web开发者往往将整个网站的所有CSS都合并到一个文件。类似的,JavaScript也被压缩到了一个文件,图片被合并到了一张雪碧图上。合并CSS、JavaScript和图片极大地减少了HTTP的请求数,在HTTP/1.1中能获得显著的性能提升。

存在的问题:

  • 高延迟:页面访问速度下降
  • 无状态:头部巨大切重复
  • 队头阻塞问题,同一连接只能在完成一个 HTTP 事务(请求和响应)后,才能处理下一个事务;
  • 明文传输:不安全
  • 不支持服务器推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量了带宽和服务器资源。

3.1.2 HTTP/2.0的优势

1、二进制分帧传输

帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧。

原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。

2、多路复用(MultiPlexing)

通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流(Stream)。HTTP/2 用流来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。

在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 Stream Identifier 标明这一帧属于哪个流,然后在对方接收时,根据 Stream Identifier 拼接每个流的所有帧组成一整块数据。 把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。

流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题。所以 http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接

HTTP/2的优化需要不同的思维方式。Web开发者应该专注于网站的缓存调优,而不是担心如何减少HTTP请求数。通用的法则是,传输轻量、细粒度的资源,以便独立缓存和并行传输。

3、服务端推送(Server Push)

在 HTTP/2 当中,服务器已经不再是完全被动地接收请求,响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 连接建立之后,比如浏览器请求一个 HTML 文件,服务器就可以在返回 HTML 的基础上,将 HTML 中引用到的其他资源文件一起返回给客户端,减少客户端的等待。

Server-Push 主要是针对资源内联做出的优化,相较于 http/1.1 资源内联的优势:

  • 客户端可以缓存推送的资源
  • 客户端可以拒收推送过来的资源
  • 推送资源可以由不同页面共享
  • 服务器可以按照优先级推送资源

4、Header 压缩(HPACK)

使用 HPACK 算法来压缩首部内容

HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

3.2 代码压缩

3.2.1开启 gzip 压缩

gzipGNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,Web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右

1、Nginx配置

gzip  on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/javascript application/x-javascript application/xml application/json;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

配置好重新启动Nginx,当看到请求响应头中有 Content-Encoding: gzip,说明传输压缩配置已经生效,此时可以看到我们请求文件的大小已经压缩很多。

2、Node 服务端

以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:

1)、npm install compression —save
2)、const compression = require('compression');
    const app = express();
	app.use(compression())
3)、重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功:

3、Webpack 压缩

以webpack 为例,可以使用如下几类插件进行压缩处理

  • JavaScript:UglifyPlugin
  • CSS :MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin

gzip 是目前最流行和最有效的压缩方法。举个例子,用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。

npm install compression-webpack-plugin —-save-dev

const CompressionPlugin = require(‘compression-webpack-plugin’);

module.exports = {
  plugins: [new CompressionPlugin()],
}

3.3 js性能优化

3.3.1减少全局变量的使用:全局变量会增加作用域链的长度,影响访问变量的速度。尽量将变量限制在局部作用域内,减少全局变量的使用。

1、使用模块化开发:使用模块化开发工具(如ES6模块、CommonJS、AMD等)将代码分割成模块,每个模块有自己的作用域,避免了全局变量的污染。

2、使用立即执行函数表达式(IIFE):使用IIFE将代码包裹起来,形成一个单独的作用域,可以防止变量污染全局作用域。

javascriptCopy code(function() { 
	// 代码
})();

3、尽量避免在全局作用域中声明变量:将变量声明在函数内部或模块内部,减少全局变量的数量。

4、使用命名空间:将相关的变量和函数放在同一个命名空间下,避免全局变量的冲突。

var myNamespace = {
    var1: 'value1',
    var2: 'value2',
    func1: function() {
        // 代码
    }
};

5、使用ES6的let和const关键字:let和const关键字声明的变量具有块级作用域,可以减少全局变量的使用。

let localVar = 'local'; const CONSTAST = xxx;

6、使用严格模式:在函数或模块的开头使用严格模式,可以提醒开发者注意避免使用全局变量。

'use strict';

3.3.2避免频繁的DOM操作:频繁的DOM操作会引起页面重绘和重排,影响性能。可以将多个DOM操作合并成一次操作,或者使用DocumentFragment来减少操作次数。

3.3.3使用事件委托:事件委托可以减少事件处理程序的数量,提高性能。将事件绑定在父元素上,通过事件冒泡机制处理子元素的事件。

<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>凤梨</li>
</ul>

<script>
// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 
</script>

3.3.4优化循环:避免在循环中进行重复的操作,尽量减少循环的次数。可以使用缓存数组长度、避免在循环内部创建新对象等方式优化循环。

var arr = [1, 2, 3, 4, 5];
for (var i = 0, len = arr.length; i < len; i++) {
    // 处理逻辑
}

3.3.5使用合适的数据结构:根据数据的特点选择合适的数据结构,如使用Map或Set代替普通对象,使用数组代替类数组对象等。

// Array
// Object
// Map
// Set
// WeakMap 和 WeakSet

3.3.6慎重使用eval()函数:eval()函数会动态执行JavaScript代码,但会影响性能并增加安全风险。尽量避免使用eval()函数。

3.3.7避免使用不必要的递归:递归调用会增加函数调用栈的深度,影响性能。尽量避免不必要的递归调用。

// 限制递归深度
// 避免递归和DOM操作混合使用

3.3.8使用事件缓存:在需要频繁绑定和解绑事件的场景中,可以使用事件缓存,避免重复绑定和解绑事件。

var button = document.getElementById('myButton');
button.addEventListener('click', handleClick);

function handleClick(event) {
    // 处理点击事件
}

3.3.9使用节流和防抖:在需要处理频繁触发的事件(如滚动、resize等)时,可以使用节流和防抖函数来控制事件触发的频率,提高性能。

// 防抖
// 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
function debounce(func, delay) {
    let time = null;
    return function (...args) {
        const context = this;
        if (time) {
            clearTimeout(time);
        }
        time = setTimeout(() => {
            func.call(context, ...args);
        }, delay);
    };
}
// 节流
// 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
function throttle(func, delay) {
    let prevTime = Date.now();
    return function (...args) {
        const context = this;
        let curTime = Date.now();
        if (curTime - prevTime > delay) {
            prevTime = curTime;
            func.call(context, ...args);
        }
    };
}

3.4 页面渲染优化

webkit渲染引擎流程:

  • 处理 HTML 并构建 DOM 树
  • 处理 CSS 构建 CSS 规则树(CSSOM)
  • 接着JS 会通过 DOM Api 和 CSSOM Api 来操作 DOM Tree 和 CSS Rule Tree 将 DOM Tree 和 CSSOM Tree 合成一颗渲染树 Render Tree。
  • 根据渲染树来布局,计算每个节点的位置
  • 调用 GPU 绘制,合成图层,显示在屏幕上

1、CSS 的阻塞

我们提到 DOM 和 CSSOM 合力才能构建渲染树。这一点会给性能造成严重影响:默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容,即便 DOM 已经解析完毕了

只有当我们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。 很多时候,DOM 不得不等待 CSSOM。因此我们可以这样总结:

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)

2、JS 的阻塞

JS 的作用在于修改,它帮助我们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。因此 JS 的执行会阻止 CSSOM,在我们不作显式声明的情况下,它也会阻塞 DOM。

JS 不仅可以读取和修改DOM 属性,还可以读取和修改CSSOM 属性,存在阻塞的 CSS 资源时, 浏览器会延迟 JS 的执行和 Render Tree 构建。

JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。

  • 现代浏览器会并行加载 JS 文件。
  • 加载或者执行JS时会阻塞对标签的解析,也就是阻塞了DOM 树的形成,只有等到JS执行完毕,浏览器才会继续解析标签。没有DOM树,浏览器就无法渲染,所以当加载很大的JS文件时,可以看到页面很长时间是一片空白

之所以会阻塞对标签的解析是因为加载的 JS 中可能会创建,删除节点等,这些操作会对 DOM 树产生影响,如果不阻塞,等浏览器解析完标签生成 DOM树后,JS 修改了某些节点,那么浏览器又得重新解析,然后生成 DOM 树,性能比较差。

实际使用时,可以遵循下面2个原则:

  • CSS 资源优于 JavaScript 资源引入
  • JS 应尽量少影响 DOM 的构建

3、 改变 JS 阻塞的方式

defer 方式加载 script, 不会阻塞 HTML 解析,等到 DOM 生成完毕且 script 加载完毕再执行 JS。

<script defer></script>

async 属性表示异步执行引入的 JS,加载时不会阻塞 HTML解析,但是加载完成后立马执行,此时仍然会阻塞 load 事件。

<script async></script>

4、使用字体图标 iconfont 代替图片图标

字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。

5、降低 CSS 选择器的复杂性

内联 > ID选择器 > 类选择器 > 标签选择器
  • 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素
  • 关注可以通过继承实现的属性,避免重复匹配重复定义
  • 尽量使用高优先级的选择器,例如 ID 和类选择器
  • 避免使用通配符,只对需要用到的元素进行选择

6、减少重绘和回流

重绘 (Repaint):当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘

回流 (Reflow):当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

回流必将引起重绘,重绘不一定会引起回流,回流比重绘的代价要更高

  • 避免频繁操作样式:将多次修改样式的操作合并成一次操作。
  • 使用CSS3动画:使用CSS3的transform和opacity属性来实现动画,减少重排和重绘次数。
  • 使用requestAnimationFrame:使用requestAnimationFrame来执行动画,浏览器会在下一次重绘前执行动画,提高性能。

7、图片资源优化

使用雪碧图:雪碧图的作用就是减少请求数,而且多张图片合在一起后的体积会少于多张图片的体积总和,这也是比较通用的图片压缩方案

降低图片质量:一是通过在线网站进行压缩,二是通过 webpack 插件 image-webpack-loader。它是基于 imagemin 这个 Node 库来实现图片压缩的。

// config/webpack.base.js

npm i -D image-webpack-loader

module: {
    rules: [
        {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: '[name]_[hash].[ext]',
                        outputPath: 'images/'
                    }
                },
                {
                    loader: 'image-webpack-loader',
                    options: {
                        // 压缩 jpeg 的配置
                        mozjpeg: {
                            progressive: true,
                            quality: 65
                        },
                        // 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
                        optipng: {
                            enabled: false
                        },
                        // 使用 imagemin-pngquant 压缩 png
                        pngquant: {
                            quality: '65-90',
                            speed: 4
                        },
                        // 压缩 gif 的配置
                        gifsicle: {
                            interlaced: false
                        },
                        // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
                        webp: {
                            quality: 75
                        }
                    }
                }
            ]
        }
    ];
}

8、使用CSS3 代替图片

有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。

9、使用 webp 格式的图片

WebP 是 Google 团队开发的加快图片加载速度的图片格式,其优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。

3.5 打包优化

以webpack为例

1、使用 SplitChunksPlugin

Webpack 4 可以在配置文件中使用该插件的配置选项optimization.splitChunks

module.exports = {
    //...
    optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        }
    }
};

Webpack 5 默认情况下会根据一些启发式规则自动分割代码,因此大多数情况下不需要手动配置 SplitChunksPlugin

module.exports = {
    //...
    optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 0,
            minRemainingSize: 0,
            minChunks: 1,
            maxAsyncRequests: 30,
            maxInitialRequests: 30,
            enforceSizeThreshold: 50000,
            cacheGroups: {
                defaultVendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    reuseExistingChunk: true,
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
    },
};
  • 配置 runtimeChunk:与 Webpack 4 类似,可以通过配置 runtimeChunk: 'single' 来将 runtime 代码提取到单独的 chunk 中。
  • 使用其他插件:在 Webpack 5 中,还可以使用 ModuleFederationPlugin 来实现更灵活的模块共享和代码分割,特别适用于微前端架构。

2、按需加载

这使得应用程序可以在需要时动态加载模块,而不是在初始加载时加载所有内容,从而提高了应用程序的性能。

// 使用动态import()来按需加载组件
const MyComponent = () => import('./MyComponent.vue');

// 在需要时使用组件
const loadComponent = async () => {
    const component = await MyComponent();
    // 使用组件
};

loadComponent();

3、配置 package.json:在 package.json 文件中,设置 "sideEffects": false,以告诉 Webpack 所有代码都没有副作用(不会影响除当前模块以外的其他模块),可以更彻底地进行 Tree Shaking。

4、避免使用 import * as module:这种方式会导入整个模块,而不是只导入需要的部分,无法被 Tree Shaking 优化。应该使用具体的命名导入。

5、使用 ES6 模块:ES6 模块静态分析更容易,因此推荐使用 ES6 模块语法,而不是 CommonJS 等其他模块系统。

6、注意第三方模块:一些第三方模块可能没有正确地标记副作用或者有副作用,需要额外的配置或者使用特定的工具来处理。

7、模版预编译

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

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // 入口文件
    entry: './src/index.js',
    // 输出配置
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    // 模块配置
    module: {
        rules: [
            // 处理  模板文件
            {
                test: /\.xxx$/,
                use: 'xxx-loader'
            }
        ]
    },
    // 插件配置
    plugins: [
        // 使用 HtmlWebpackPlugin 插件生成 HTML 文件
        new HtmlWebpackPlugin({
            template: './src/template.xxx', // 模板文件路径
            filename: 'index.html' // 输出的 HTML 文件名
        })
    ]
};

3.6 nuxt  & Vue性能优化

1、服务端渲染(对于 Nuxt 项目):使用 Nuxt.js 的服务端渲染功能,提高首屏加载速度和 SEO。

1.1)优化页面内容:确保每个页面都有明确的标题(<title>)和描述(<meta name="description">),这有助于搜索引擎正确地索引页面内容。

1.2)使用静态生成(generate):对于不经常更新的页面,可以使用 Nuxt.js 的静态生成功能。这样可以在构建时生成静态 HTML 文件,加快页面加载速度。

1.3)优化图片加载:使用适当大小和格式的图片,并考虑使用懒加载或渐进式加载来提高页面加载速度。

  • 使用loading="lazy"属性:对于支持的浏览器,可以直接在<img>标签中添加loading="lazy"属性,配合vue-lazyload插件
  • 预加载重要图片:对于某些重要的图片(如首屏展示的图片),可以使用<link rel="preload">标签或者 JavaScript 提前加载,以确保用户能够快速看到这些图片
  • 使用Intersection Observer:Intersection Observer 是一种现代的浏览器 API,可以监视元素与视口的交叉并触发回调。可以使用 Intersection Observer 来实现图片懒加载。
const images = document.querySelectorAll('img[data-src]');
const config = {
    rootMargin: '0px 0px 50px 0px',
    threshold: 0
};
const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
        }
    });
}, config);

images.forEach(image => {
    observer.observe(image);
});

2、使用 CDN 加速:将静态资源(如图片、字体等)托管到 CDN 上,加速资源加载速度。

  • 多个域名并行加载:利用 CDN 的域名分发功能,将资源分布到多个域名下并行加载,提高资源加载速度。
  • 优化图片和静态资源:对图片和其他静态资源进行优化,使用合适的格式和大小,并利用 CDN 的压缩功能。
  • 使用 HTTP/2:使用支持 HTTP/2 的 CDN,以提高资源加载速度和性能。
  • 合理使用预加载和预解析:对于重要的资源或者跨域请求,可以使用预加载和预解析技术,提前加载和解析资源,加快页面加载速度。

3、启用缓存:使用合适的缓存策略,减少重复请求,提高加载速度。

  • 合理缓存设置:配置 CDN 缓存策略,根据资源的特性设置不同的缓存时间,以减少源服务器的负载并提高访问速度。
  • 合理使用缓存刷新:在更新静态文件时,使用合适的缓存刷新策略,避免不必要的缓存失效。

3.8 浏览器缓存

浏览器缓存是提高网站性能的关键因素之一。通过合理配置缓存策略,可以减少不必要的网络请求,加快页面加载速度。

  • 使用缓存头部:通过在服务器端设置合适的缓存头部来控制浏览器缓存。常用的缓存头部包括Cache-ControlExpiresLast-ModifiedETag
  • Cache-Control:Cache-Control 是最常用的缓存头部之一,用来指定资源的缓存策略。常用的指令包括:
  1. public:表示资源可以被所有用户缓存,适用于静态资源。
  2. private:表示资源只能被单个用户缓存,适用于私有页面。
  3. max-age=<seconds>:指定资源在缓存中的最长时间,单位为秒。
  4. no-cache:表示缓存资源需要重新验证,不直接使用缓存的副本。
  5. no-store:表示不缓存任何资源。
Cache-Control: max-age=3600, public

Expires:Expires 是指定资源过期时间的一种方式,它是一个 HTTP 头部,表示资源在客户端缓存的有效期限。但是,它受限于本地时间,可能存在时钟不同步等问题,建议使用Cache-Control来替代。

Expires: Wed, 21 Oct 2026 07:28:00 GMT

Last-Modified 和 If-Modified-Since:Last-Modified是指资源的最后修改时间,If-Modified-Since是客户端发送的请求头部,用于判断资源是否已经被修改过。服务器可以通过比较资源的修改时间来判断是否返回304 Not Modified状态码,从而减少传输数据量。

Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT

ETag 和 If-None-Match:ETag是服务器生成的资源唯一标识符,If-None-Match是客户端发送的请求头部,用于判断资源是否发生了变化。服务器可以通过比较资源的ETag来判断是否返回304 Not Modified状态码。

ETag: "686897696a7c876b7e"
If-None-Match: "686897696a7c876b7e"

版本号管理:对于静态资源,可以通过在文件名或路径中添加版本号或哈希值的方式来管理版本,以便更新文件内容后能够及时生效。

entry:{
    main: path.join(__dirname,'./main.js'),
    vendor: ['react', 'antd']
},
output:{
    path:path.join(__dirname,'./dist'),
    publicPath: '/dist/',
    filname: 'bundle.[chunkhash].js'
}
  • hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。
  • chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
  • contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。
    显然,我们是不会使用第一种的。改了一个文件,打包之后,其他文件的hash都变了,缓存自然都失效了。这不是我们想要的。

chunkhashcontenthash的主要应用场景是什么呢?在实际在项目中,我们一般会把项目中的 CSS 都抽离出对应的 CSS 文件来加以引用。如果我们使用chunkhash,当我们改了CSS 代码之后,会发现 CSS 文件hash值改变的同时,JS 文件的hash值也会改变,这种场景下就需要配置为contenthash

总结:

前端性能优化是为了提升用户体验和网站的整体性能,可以加快页面加载速度,减少资源消耗,提高页面响应速度,从而使用户能够更快地访问到所需内容,提高用户满意度和留存率、优化还可以降低服务器负载和带宽成本,提高网站的可扩展性和可维护性、对于前端工作而言,优化是必不可少的工作内容,也是前端开发者的必备技能。

展示评论