作者:Bonnie
https://juejin.cn/post/7363830946908979239
在当今数字时代,网站的性能对于吸引和保留用户至关重要。用户不愿意等待缓慢的加载时间,而快速响应的页面将帮助您留住访问者,提升转化率。前端性能优化是实现这一目标的关键因素之一。在本文中,我们将探讨一些重要的前端性能优化策略,以提高网站速度、交互性和用户满意度。
loading-is-a-journey.png现在将以 Performance Observer 为例,详细讨论几个重要的性能指标的具体实现
LCP 分析会考虑到其找到的所有内容,甚至包括已从 DOM 中删除的内容。每当发现新的最大内容时,它都会创建一个新条目,因此可能会存在多个对象。然而,当发生滚动或输入事件时,LCP 分析会停止搜索更大的内容。因此,一般来说,LCP 数据会取最后一个找到的内容作为结果。
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(lastEntry);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
LCP-object.png
以下是对于给定指标的解释和展示对象的描述:
在这个示例中,LCP 为 loadingTime,即 1.6。根据上述度量标准,这被认为是良好的。这表示在视图中最大的内容元素(图片)在 1.6 秒内成功渲染,符合较好的用户体验标准。
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});
observer.observe({ type: 'paint', buffered: true });
在给定的指标中:
在本示例中,FCP 为 startTime ,即小于 1 秒。根据提供的标准,这被认为是良好的。
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
const FID = entry.processingStart - entry.startTime;
console.log(entry);
});
});
observer.observe({ type: 'first-input', buffered: true });
FID-object.png
在给定的指标中:
在示例代码中,FID 等于 8574(processingEnd) - 8558(processingStart) = 16。根据提供的标准,这被认为是良好的。
NIP 仅会受到以下事件的影响:
与 FID 的关系:
INP 考虑了所有页面交互,而 FID 仅考虑第一次交互。INP 不仅仅关注于首次交互,而是通过对所有交互进行抽样,以全面评估响应能力,使 INP 成为比 FID 更可靠的整体响应能力指标。
由于 Performance API 中没有提供 INP 的响应能力,因此这里不提供具体示例。有关如何测量此指标的信息,请点击这里[8]。
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});
observer.observe({ type: 'layout-shift', buffered: true });
其中有几个指标:
在本示例中,CLS 为 value, 即 0。根据提供的标准,这被认为是良好的。
主要原因可能是:
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});
observer.observe({ type: 'longtask', buffered: true });
其中几个指标解释如下:
因为长任务(Long Task)对用户体验有显著影响,所以即使它不是 Web Vitals 的一部分,也将其单独列出来
懒加载和代码分割都是用于优化前端性能的策略,但它们有不同的目标和应用方式。
复制代码
目标:代码分割的主要目标是减小初始加载时所需的 JavaScript 文件大小,以提高页面的初始加载速度。它将应用的代码分成多个块,通常基于路由或功能,以便按需加载。
复制代码
目标:懒加载是一种策略,它使您能够将某些组件、资源或功能推迟加载,而不是在初始加载时加载它们。这有助于减少初始加载时的资源负担,提高页面速度。
在 React 中,这一概念主要体现在将代码拆分(code split)与懒加载(lazy load)相结合
const DownloadFile = lazy(() => import('./page/OperateFile/OperateFile'));
const TimeSelect = lazy(() => import('./page/TimeSelect/TimeSelect'));
export const router: Router[] = [
{
path: '/',
element: <App />,
name: 'Home',
},
{
path: '/download-file',
element: <DownloadFile />,
name: 'Download File',
},
{
path: '/time-select',
element: <TimeSelect />,
name: 'Time Select',
},
];
通常,我们根据资源的更新频率来配置合适的指令,以从缓存中获取资源,从而降低请求频率并提升加载效率。这涉及到针对响应(response)、请求(request)的具体配置。具体的配置指令可能因情况而异,详细内容请点击这里[10]。
Cache-Control: no-cache
表示会缓存,但在使用之前将先向服务器验证是否为最新数据。如果客户端已经是最新的,通常响应将返回 304(Not Modified);反之,将使用新的数据。这种做法确保每次获取的都是最新的响应。由于大多数 HTTP 1.0 不支持 no-cache,我们可以采用一种 fallback 方案。
Cache-Control: max-age=0, must-revalidate
这里有一个额外的细节,通常我们还会添加以下信息:如果资源属于用户个人内容,可以将其指定为 private;反之则为 public。判断资源是否为个人数据的方法之一是查看请求头中是否包含 Authorization 字段,如果有,则意味着这是个人数据,通常就无需额外指定为 private 了。此外,如果缓存控制头中包含 must-revalidate,也标识这是个人数据。这表示每次获取资源之前需要验证其是否为新的资源,如果是,就使用新的;如果不是,则使用已缓存的旧数据。这种方式有助于确保对个人数据的实时性和一致性
Cache-Control: public, immutable, max-age=31536000
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
ETag、Last-Modified 和 Immutable 可以阻止资源重新验证,尤其在重新加载页面时。这些机制有助于优化缓存管理,确保资源的一致性和有效性。
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
在一些场景中,Cache-Control 可能会同时出现在 request 和 response 上,而在发生冲突时,通常会以 response 上的设置为准。
内容分发网络(CDN)是一个分布式的服务器网络,它缓存来自源服务器的资源,并通过更接近用户地理位置的服务器提供这些资源。通过降低往返时间(RTT),以及采用 HTTP/2 或 HTTP/3、缓存和压缩等优化策略,CDN 能够更快地提供内容,改善用户的访问体验,具体可以点击这里[12]了解。
关于 代码的最小化和压缩,目前我们使用 Terser 工具来实现,主要包括移除未使用的代码(Tree Shaking)、缩短变量名以及删除空格、Uglifiers 等操作。这一优化手段在 Rollup.js 和 Webpack 中都得到了应用,以降低代码体积、减少下载时间。
对于 CSS,在 Webpack 中,通常会利用mini-css-extract-plugin
插件进行优化。该插件能够独立地从每个包含 CSS 的 JavaScript 文件中提取出一个单独的 CSS 文件,实现样式的独立加载。更进一步,该插件支持按需加载和 Source Map,为样式管理提供了更加灵活和高效的方式。
对于图片资源,使用 WebP 格式代替 JPEG 和 PNG 可以显著减少文件大小,通常能够实现 25%-35% 的减少。同时,使用内容交付网络(CDN)对于图片加载的优化效果显著,通常能够实现 40%-80%的图片大小节省。为了考虑兼容性,可以采用以下方式来实现
<picture>
<source type="image/webp" srcset="flower.webp">
<source type="image/jpeg" srcset="flower.jpg">
<img src="flower.jpg" alt="">
</picture>
前端每次请求资源时,涉及建立一个 TCP 连接,完成请求后即会关闭该 TCP 连接。如下图所示:
HTTP 协议经历了多次版本的更新,主要包括 HTTP/1.0、HTTP/1.1 和 HTTP/2.0。以下是不同版本的 HTTP 在请求方面的一些关键差异,以图表形式展示:
对于 Web 开发者而言,采用 HTTP/3 并未带来过多的变化,因为 HTTP/3 仍然遵循 HTTP 协议的核心原则。通过 QUIC(Quick UDP Internet Connections)协议的支持,HTTP/3 在连接建立时延方面提供了更低的延迟,改善了多路复用效率,并引入更灵活的流控制机制。鉴于这些优势,HTTP/3 在性能方面有所提升。然而,由于 HTTP/3 的实现主要发生在协议层面,对于 Web 开发者来说,通常无需进行大规模的应用程序更改,所以并没有把 HTTP/3 列入比较。。
同时值得注意的是,HTTP/2.0 引入了 Server Push 功能,这对于改善前端性能非常有利。Server Push 允许服务器主动将资源推送给前端,例如,在客户端请求 HTML 文件时,服务器可以直接将 CSS 和 JavaScript 资源主动推送给客户端,省去了客户端发起请求的时间。
然而,需要注意的是,由于 Server Push 机制的一些限制,目前 Chrome 浏览器并不支持 HTTP/2 Server Push 功能。详细的支持情况可以查看此链接[13]。尽管如此,开发者仍然可以利用其他性能优化手段,例如资源合并、缓存策略等,以提高前端加载性能
Http-server-push.png上述描述表达了 HTTP 协议发展的演进,其目的都在于缩短加载时间,提高请求效率。在这一过程中,出现了一些传统的性能优化技术,例如资源内联(resource inlining)和图像精灵(image spriting),它们通过将多个小文件捆绑成一个大文件,并在单个连接上传输,有助于减少传输的头部开销,从而提高性能。在 HTTP/1.0 和 HTTP/1.1 时代,这样的技术被认为是有效的性能优化实践。
然而,随着 HTTP/2.0 的引入,这一情况发生了变化。HTTP/2.0 允许在同一连接上同时请求多个资源,而无需每个资源都建立独立的 TCP 连接。这一特性使得捆绑优化等“hack”技术在 HTTP/2.0 时代变得不再那么必要,因为单一连接上的多路复用大大提高了资源的并行传输效率。因此,在 HTTP/2.0 时代,我们不再迫切需要依赖这些传统的性能优化技术,而可以更专注于其他方面的性能优化,以更好地适应新的协议特性。在项目中,很多开发者可能已经减少或不再使用这样的技术,而将注意力集中在更为有效的性能优化手段上。
渲染路径如下图所示,可以观察到 CSS 和 JavaScript 会阻塞渲染,因此需要根据业务的重要性来识别并优化关键资源的加载顺序,以提升加载时间。目前存在一个非标准的属性 blocking=render,允许开发者明确地将<link>
、<script>
或<style>
元素标记为在该元素被处理之前阻塞渲染,但同时允许解析器在此期间继续处理文档
帮助开发者通过告知浏览器如何加载和设置资源优先级,进一步优化页面加载时间,具体操作如下:
为了兼容性,建议结合使用 DNS Prefetch 和 preconnect,但需要谨慎配置,避免过度使用以防资源浪费
tsx
复制代码
<link rel="preconnect" href="https://third-party-domain.com" />
<link rel="dns-prefetch" href="https://third-party-domain.com" />
测试效果如图
compared-pre-render.png重要资源
,如关键 CSS 或影响 LCP 的图片等。async
和defer
允许外部脚本在加载时不阻塞 HTML 解析器,而带有type="module"
的脚本(包括内联脚本)会自动被延迟执行。
###Fetch Priority API**
您可以通过 Fetch Priority API 的 {% mark fetchpriority color:green %}属性来提高资源的优先级。您可以在<link>
、<img>
和<script>
元素中使用该属性。
loading 属性告知浏览器如何加载图片。
fetchpriority 属性可指定图片加载的优先级
通过根据图片的业务价值使用这些属性,可以优化 Web Core Vitals 指标,提升整体性能。此外,提前加载关键图片资源也可使用link
** 标签。
<link rel="preload" fetchpriority="high" as="image" href="image.webp" type="image/webp">
- 图像不应提供大于用户屏幕上呈现的版本。
- 使用响应式图像,指定多个图像版本,浏览器会选择使用最佳版本。
<img src="flower-large.jpg" srcset="example-small.jpg 480w, example-large.jpg 1080w" sizes="50vw">
480w 是指告知浏览器在不需要下载图片的情况下,就知道宽度是 480px;sizes 指定图片预期显示大小可以使用 svg,可以无限缩放
4. width 和 height:
````css
- 应该同时指定适当的`width`和`height` 属性,以确保浏览器在布局中分配正确的空间。这有助于避免布局偏移,提高 Cumulative Layout Shift(CLS)的用户体验。
- 如果无法确定具体的宽度和高度,可以考虑设置宽高比例,以提供一种解决方案
img {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
```
- {% mark sync color:green %}:同步解码图像,以便与其他内容一同呈现。
- {% mark async color:green %}:异步解码图像,并允许在其完成之前呈现其他内容。
- {% mark auto color:green %}:对解码模式没有偏好;浏览器决定对用户最有利的方式。这是默认值,但不同的浏览器有不同的默认值:
Chromium 默认为 sync,Firefox 默认为 async,Safari 默认为 sync。`decoding` 属性的效果可能仅在非常大、高分辨率的图像上才会显著,因为这些图像的解码时间较长。
对于图片资源,需要根据具体业务需求来选择合适的图像格式以优化性能。以下是简化和优化表达的建议:
主要的图片格式:
属性是为了向浏览器提供有关作者认为在视频播放之前加载哪些内容会导致最佳用户体验的提示。它可以具有以下值:
每个浏览器的默认值不同。规范建议将其设置为 metadata。具体来说比如想要推迟视频的加载,可以写成这样:
<video controls preload="none" poster="placeholder.jpg">
<source src="video.mp4" type="video/mp4">
<p>
Your browser doesn't support HTML video. Here is a
<a href="myVideo.mp4" download="myVideo.mp4">link to the video</a> instead.
</p>
</video>
在相同的视觉质量下,视频文件通常比 GIF 图像更小。以下示例展示了懒加载视频并自动播放。通过使用 IntersectionObserver 监测视频是否进入可视范围,并在需要时进行加载和播放。这样做可以提高首次加载的时间。
//playsinline 兼容自动播放在ios上
//poster 一个video的placeholder
<video class="lazy" autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
<source src="one-does-not-simply.webm" type="video/webm">
<source src="one-does-not-simply.mp4" type="video/mp4">
</video>
document.addEventListener("DOMContentLoaded", function() {
var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
if ("IntersectionObserver" in window) {
var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(video) {
if (video.isIntersecting) {
for (var source in video.target.children) {
var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src;
}
}
video.target.load();
video.target.classList.remove("lazy");
lazyVideoObserver.unobserve(video.target);
}
});
});
lazyVideos.forEach(function(lazyVideo) {
lazyVideoObserver.observe(lazyVideo);
});
}
});
如果视频作为 Largest Contentful Paint (LCP) 元素,可以预先请求 poster placeholder 图片,这样有助于提升 LCP 性能。
<link rel="preload" as="image" href="poster.jpg" fetchpriority="high">
在服务器上执行客户端应用程序逻辑,并生成包含完整 HTML 标记的响应,以响应 HTML 文档的请求。通过在服务端请求相关资源文件,SSR 提高了首屏加载速度并增强了搜索引擎优化(SEO)效果。尽管 SSR 需要额外的服务器处理时间,并且每次重新请求都需要重新生成,但通常这种权衡是值得的。因为服务器处理时间是在开发者的控制范围内,而用户的网络和设备性能则不可控制。在实践中,SSR 的优势往往超过了其缺点,特别是在考虑到改善用户体验和搜索引擎排名的情况下
在构建时编译和呈现网站程序的过程。它生成一系列静态文件,包括 HTML 文件、JavaScript 和 CSS 等资产。这些静态文件在每次请求时被重复利用,无需重新生成。通过将静态生成的页面缓存到 CDN 中,可以在不需要额外配置的情况下提高性能。
SSG 的主要应用场景是对于所有用户而言,渲染的页面内容都是相同的。因此,对于博客、文档站点等内容相对固定的网站,SSG 是一种非常合适的方式。在构建时进行预渲染,生成静态文件,使得这些文件可以被缓存,提供快速的访问体验。这种静态生成的方式适合不经常变化的内容,从而减少了服务器运行时的负担,同时提供了更好的性能。
requestAnimationFrame
浏览器会在下次重绘时调用该方法,相较于 setInterval
或 setTimeout
,它能够更智能地在浏览器的帧渲染中进行优化。使用 setInterval
或 setTimeout
有可能导致回调在帧的某个点运行,可能在帧的末尾,这通常导致错过一帧,从而导致界面卡顿。而 requestAnimationFrame
可以确保回调在浏览器准备好进行下一次重绘时执行,使得动画效果更加流畅。
Long Task 指的是执行时间超过 50 毫秒的任务,可以通过以下方式在 Main Thread 上释放:
Web Workers 是在后台运行的独立线程,拥有自己的堆栈、堆内存和消息队列。与主线程进行通信只能通过postMessage
** 方法发送消息,而无法直接操作 DOM。因此,Web Workers 极为适合执行那些不需要与 DOM 直接交互的任务。例如,对大规模数据进行排序、搜索等操作可以放在 Web Worker 中执行,从而避免了这些计算密集型任务对主线程的阻塞,确保主线程保持响应性。通过将这些耗时任务放在 Web Worker 中执行,不仅可以提高主线程的性能和响应性,还能更好地利用多核处理器的性能优势。这种分离计算任务与用户界面操作的方式有助于改善整体的用户体验,确保页面流畅运行。
Service Worker 是一种在后台运行的脚本,用于拦截和处理网络请求。通过合理利用 Service Worker,可以对资源进行缓存,从而减少对主线程的依赖,提高应用程序的性能。
为了确保长时间运行的任务不会阻塞主线程,我们可以采用将这些长任务拆分成小的、异步执行的子任务的策略。可以采用一下策略:
function yieldToMain() {
//Wrapping with Promise is for presenting it in a synchronous manner."
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
//isInputPending is true when user attempts to interact with the page
// performance.now() >= deadline is isInputPending fallback
if (
navigator.scheduling?.isInputPending() ||
performance.now() >= deadline
) {
await yieldToMain();
deadline = performance.now() + 50;
continue;
} else {
otherTask();
}
需要注意的是微任务并不会释放主线程。例如,当使用 Promise 创建一个微任务时,它会被放入微任务队列中,等待主线程执行完毕后立即执行。即使通过 queueMicrotask
创建的微任务,也会作为第一个执行。这导致主线程会在执行微任务时保持繁忙,不会释放去执行其他任务。
详细的可视化展示可以在这里查看[16]。
这种机制在处理异步任务时非常重要,因为它确保微任务中的逻辑在当前任务结束后立即执行。这对于处理 Promise 或其他异步操作的结果非常有用,但需要注意它并不会释放主线程。
requestIdleCallback
是一种优化手段,可在 main thread 空闲时调度执行低优先级或后台任务,以提高页面的响应性。这种方法有助于确保任务的执行不会干扰用户交互和页面渲染,而是在主线程空闲时进行。举例来说,React 的虚拟 DOM 机制采用了批处理的优化策略。它通过将所有变化应用于虚拟 DOM,然后一次性提交给浏览器进行重绘,从而极大地减少了对实际 DOM 的操作。这种方式有效释放主线程,提升性能。这样的批处理机制在 DOM 操作较多或变化频繁的情况下,通过将多个操作合并为一个批次来减少浏览器的重绘次数,从而优化了性能。在 React 中,这一机制有助于提高页面的响应性,避免不必要的重复计算和渲染。
前端性能优化是一个持续的过程,需要不断的关注和改进。通过综合考虑上述策略,您可以提高网站的速度、交互性和用户满意度。不断关注性能,使用工具来评估和监测网站性能,然后采取适当的措施来改进,将有助于确保您的网站始终能够提供出色的用户体验。在当今竞争激烈的互联网环境中,前端性能优化是取得成功的不可或缺的一环。