前端性能优化

28 阅读9分钟

前言

性能优化是一个综合课题,涉及的知识面非常广。

首先我们应该要搞清楚浏览器渲染页面的流程,然后对中间的步骤进行针对性优化。

一、加载优化

资源请求优化

域名发散(避免6个下载数量限制)、减少http请求数、cdn加速、启用gzip、启用http2、启用http缓存、并发加载(async/defer);

打包优化

  • 减小资源体积和数量:
  • tree-shaking:
  • 关闭sourcemap:
  • 代码拆分:
  • 代码压缩:
  • 持久化缓存:
  • 监测与分析:
  • 按需加载:

懒加载

分离非首屏组件和资源并延迟加载;

按路由分割;

按组件分割;

组件懒加载;

图片懒加载;

预加载

在适当时机提前加载非首屏组件和资源;

preload、prefetch;

用 preload 预加载页面资源

组件预加载;

缓存

浏览器缓存、ServiceWorker等;

离线包

骨架屏

降低用户等待焦虑;

图片优化

图片懒加载

<img loading="lazy" src="./images/1.jpg" width="300" height="450" />

CSS Sprites减少图片资源

svg替换图片

位图首选WebP,对不支持的浏览器场景进行兼容处理

尽量为位图图像格式找到最佳质量设置

图片压缩

响应式图片(多倍图)

srcset和sizes;

媒体查询;

.css{/* 普通显示屏(设备像素比例小于等于1.3)使用1倍的图 */ 
    background-image: url(img_1x.png);
}

@media only screen and (-webkit-min-device-pixel-ratio:1.5){
.css{/* 高清显示屏(设备像素比例大于等于1.5)使用2倍图  */
    background-image: url(img_2x.png);
  }
}

二、渲染优化

js优化

不使用的变量设置为null

合理使用闭包

借助事件委托减少事件监听

循环优化

使用break、continue降低js执行代码次数。

webworker执行耗时任务

时间分片避免js长任务阻塞渲染

promise.all替换串行await

css优化

transform: translateZ(0)

backface-visibility: hidden

内容可见性(content-visibility)

合理使用will-change

让元素及其内容尽可能独立于文档树的其余部分(contain)

使用font-display解决由于字体造成的布局偏移(FOUT)

scroll-behavior让滚动更流畅

开启GPU渲染

浏览器针对处理CSS动画和不会很好地触发重排(因此也导致绘)的动画属性进行了优化。为了提高性能,可以将被动画化的节点从主线程移到GPU上。将导致合成的属性包括 3D transforms (transform: translateZ(), rotate3d(),等),animatingtransformopacity, position: fixedwill-change,和 filter。一些元素,例如 <video>, <canvas><iframe>,也位于各自的图层上。 将元素提升为图层(也称为合成)时,动画转换属性将在GPU中完成,从而改善性能,尤其是在移动设备上。

拆分样式文件

避免@import包含多个样式表

减少重排重绘

长列表优化

时间分片;

虚拟列表 + webWorker + indexedDB;

服务端渲染SSR

三、分类优化

包含 7 个类别共 35 条前端性能优化最佳实践:

四、性能指标

可以通过 Audit 工具获得网站的多个指标的性能报告;

可以通过 Performance 工具了解网站的性能瓶颈;

可以通过 Performance API 具体测量时间;

对性能指标计算

采集的性能指标都是某个时间点的原始数据,还需要对采集的指标转化为衡量页面性能的指标。

image.png

/**
 * @description 转换原始性能数据为统计需要的性能指标
 */
function transferOriginPerform(t) {
	const times = {}
	if (!t) return times
	// 旧版sdk上报计算后的数据,则直接返回
	if (t.fetchStart === undefined || t.connectStart === undefined) return t
	// DOM节点解析完成需要的时间
	times.domReady = Math.round(t.domComplete - t.responseEnd)
	// DCL需要的时间
	times.dclTime = Math.round(t.domComplete - t.responseEnd)
	// 页面加载到SDK开始收集信息需要的时间
	// times.loadSDK = t.loadSDK
	// 首字节,对用户来说一般无感知,对于开发者来说,则代表访问网络后端的整体响应耗时。
	times.fbTime = Math.round(t.responseStart - (t.navigationStart || t.fetchStart))
	// 白屏时间,用户看到页面展示出现一个元素的时间,区别于首字节,头部资源还没加载完毕前,页面也是白屏
	times.blankTime = Math.round((t.domInteractive || t.domLoading) - t.fetchStart)
	// 首屏时间(FMP),暂取FCP首次有内容的渲染时间
	times.fmpPaintTime = t.firstContentful_startTime || Math.round(t.domContentLoadedEventEnd - t.fetchStart)
	// 总下载时间,总下载时间即window.onload触发的时间节点, 资源同步加载完成的时间
	times.downloadTime = Math.round(t.loadEventEnd - t.fetchStart)
	// 重定向时间
	times.redirectTime = Math.round(t.redirectEnd - t.redirectStart)
	// DNS解析时间
	times.domainLookupTime = Math.round(t.domainLookupEnd - t.domainLookupStart)
	// TCP完成握手时间
	times.connectTime = Math.round(t.connectEnd - t.connectStart)
	// HTTP请求响应完成时间
	times.httpResTime = Math.round(t.responseEnd - t.requestStart)
	// DOM加载完成时间
	times.domReadyTime = Math.round(t.domComplete - t.domInteractive)
	// DOM结构解析完成时间
	times.domInteractiveTime = Math.round(t.domContentLoadedEventStart - t.domInteractive)
	// 脚本加载时间
	times.domContentLoadedTime = Math.round(t.domContentLoadedEventEnd - t.domContentLoadedEventStart)
	// onload事件时间
	times.onloadEventTime = Math.round(t.loadEventEnd - t.loadEventStart)

	// 页面加载完成时间
	times.pageLoadTime =
		times.domainLookupTime +
		times.connectTime +
		times.httpResTime +
		times.domReadyTime +
		times.domInteractiveTime +
		times.domContentLoadedTime +
		times.onloadEventTime

	return times
}

TTFB(首字节时间)

首字节,对用户来说一般无感知,对于开发者来说,则代表访问网络后端的整体响应耗时。

times.fbTime = Math.round(t.responseStart - (t.navigationStart || t.fetchStart))

FP(首次绘制时间)

  • 首次渲染(FP)用于衡量用户从打开页面到首个像素渲染到页面的时间。
  • FP通常反映页面的白屏时间,在FP时间点之前,用户看到的是没有任何内容的白色屏幕。白屏时间反映当前Web页面的网络加载性能情况,FP越短,白屏时间就越短,用户页面加载体验就越好。
times.blankTime = Math.round((t.domInteractive || t.domLoading) - t.fetchStart)

FCP(首次有内容绘制时间)

  • 首次内容渲染(FCP)用于衡量从用户首次导航至网页到页面上任意一部分内容呈现在屏幕上的时间。此处的“内容”是指文本、图像(包括背景图像)、<svg>元素或非白色<canvas>元素。
  • FCP通常反映页面首次呈现内容的时间。在FCP时间点之前,用户所见为没有任何实际内容的屏幕。首次呈现内容的时间能够反映当前Web页面的网络加载性能、页面DOM结构的复杂性以及内联脚本的执行效率等因素。FCP时间越短,首次呈现内容的时间也越短,从而提升用户的页面加载体验。
  • 注意:FCP包括上一个网页的所有卸载时间、连接设置时间、重定向时间和首字节时间(TTFB),在真实场景下,这些时间可能难以准确预估,从而导致实际测量结果与理论预期结果之间存在差异。
times.fmpPaintTime = t.firstContentful_startTime || Math.round(t.domContentLoadedEventEnd - t.fetchStart)

FMP(首次有意义绘制时间)

指页面关键元素渲染时间。这个概念并没有标准化定义,因为关键元素可以由开发者自行定义——究竟什么是“有意义”的内容,只有开发者或者产品经理自己了解。

times.fmpPaintTime = t.firstContentful_startTime || Math.round(t.domContentLoadedEventEnd - t.fetchStart)

LCP(大量有意义绘制时间)

  • LCP(Largest Contentful Paint)指的是可见视口中最大图片、文本块或视频的渲染时间(相对于用户首次导航至网页的时间)。
  • 注意:LCP包含前一页面的所有卸载时间、连接设置时间、重定向时间以及首字节时间(TTFB)。在实际测量过程中,这可能导致测量结果与理论预期结果之间存在差异。

用于衡量标准报告视口内可见的最大内容元素的渲染时间。为了提供良好的用户体验,网站应努力在开始加载页面的前 2.5 秒内进行 最大内容渲染 。

首屏时间

对于所有网页应用,这是一个非常重要的指标。用大白话来说,就是进入页面之后,应用渲染完整个手机屏幕(未滚动之前)内容的时间。需要注意的是,业界对于这个指标其实同样并没有确切的定论,比如这个时间是否包含手机屏幕内图片的渲染完成时间。

times.pageLoadTime =
		times.domainLookupTime +
		times.connectTime +
		times.httpResTime +
		times.domReadyTime +
		times.domInteractiveTime +
		times.domContentLoadedTime +
		times.onloadEventTime

TTI(用户可交互时间)

顾名思义,也就是用户可以与应用进行交互的时间。一般来讲,我们认为是 domready 的时间,因为我们通常会在这时候绑定事件操作。如果页面中涉及交互的脚本没有下载完成,那么当然没有到达所谓的用户可交互时间。

times.domReadyTime = Math.round(t.domComplete - t.domInteractive)

总下载时间

页面所有资源加载完成所需要的时间。一般可以统计 window.onload 时间,这样可以统计出同步加载的资源全部加载完的耗时。如果页面中存在较多异步渲染,也可以将异步渲染全部完成的时间作为总下载时间。

times.downloadTime = Math.round(t.loadEventEnd - t.fetchStart)

五、通过chrome调试器判断性能

如下图所示:

  1. 在chrome浏览器的开发过程中,我们会看到network面板中有这两个数值,分别对应网 络请求上的标志线,这两个时间数值分别代表什么?
  2. 我们一再强调将css放在头部,将js文件放在尾部,这样有利于优化页面的性能,为什么这种方式能够优化性能?
  3. 在用jquery的时候,我们一般都会将函数调用写在ready方法内,这是什么原理?
  4. load事件、onload事件有什么区别?

Load事件: 页面加载完成时间。load 应该仅用于检测一个完全加载的页面, 当一个资源及其依赖资源已完成加载时,将触发load事件。也就是说,页面的html、css、js、图片等资源都已经加载完之后才会触发 load 事件。

问题: 我想将埋点sdk放到load事件后加载,该怎么做呢?

window.addEventListener('load', () => {
    // 引入神策全埋点
    import('@/utils/event-track').then(({ eventTrack }) => {
        eventTrack.init()
        ;(window as any)?.sensors?.quick('autoTrackSinglePage')
    })
})

DOMContentLoaded事件: DOM Ready时间。当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。也就是说,DOM 树已经构建完毕就会触发 DOMContentLoaded 事件。

document.addEventListener("DOMContentLoaded", () => {
  console.log("Hello World!");
});

DOMContentLoaded顾名思义,就是dom内容加载完毕。那什么是dom内容加载完毕呢?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成。

参考:

2020前端性能优化清单之一

前端tree组件,10000个树节点,从14.65s到0.49s

腾讯文档表格内存优化总结

Web前端性能优化深度解读,这些细节千万不能忽视

异步分片计算在腾讯文档的实践

前端性能优化 -- 从 10 多秒到 1.05 秒

淘宝承接页是如何实现秒开的

JavaScript 启动性能瓶颈分析与解决方案

首页秒开实践指南

前端虚拟列表的实现原理

实现数据滚动显示_高性能渲染十万条数据(虚拟列表)

90行代码,15个元素实现无限滚动

性能优化之长列表渲染——时间分片和虚拟列表

2021 年 Web 核心性能指标是什么?谷歌工程师告诉你,FMP 过时啦!