前端性能优化,一直是前端开发者绕不过的一个话题, 前端网页如果存在性能问题,对于企业业务来讲,影响很大。
一是加载慢,用户会很容易跳出。
二是搜索引擎会判断为低质网页,会进行降权处理。
本文会系统的聊一下性能优化的思路,具体的实践会专门写一篇文章来说明。
FP代表浏览器第一次在页面上绘制的时间,这个时间仅仅是指开始绘制的时间,但是未必真的绘制了什么有效的内容。
FCP代表浏览器第一次绘制出DOM元素(如文字、input标签等)的时间。FP可能和FCP是同一个时间,也可能早于FCP,但一般来说两者的差距不会太大。
FMP,第一次有意义的绘制是一个主观的指标,在一般情况下,首屏指的主要是FMP。FP和FCP虽然都可以根据一个确定的规则测算出一个客观的指标值,但开始渲染和渲染第一个元素对于用户来说未必有意义。更主要的是,仅仅依靠这些指标往往容易导致度量的结果失真。
最简单的方案就是记录关键逻辑的时间点。关键逻辑的时间点包括页面关键组件渲染完成的时间、API加载等逻辑完成的时间。可以手动使用JavaScript记录时间点,从而将其作为FMP的时间。
FPS是打游戏的同学应该很熟悉的一个概念帧率
, 既 “是每秒渲染的帧数”。
每秒60帧是人眼感觉比较流畅的的帧数。同时也是目前显示器最常见的帧率。因此1000ms / 60 = 16.7ms
, 60Hz刷新率的情况下,每一帧不于16ms就不会感觉到卡顿。
根据wikipedia
人类视觉的时间敏感性和分辨率根据视觉刺激的类型和特征而变化,并且在个体之间是不同的。由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10至12张的时候,就会认为是连贯的,此现象称之为视觉暂留。这也就是为什么电影胶片是一格一格拍摄出来,但是借由快速播放,能让画面看起来是连续的。
当调制光(如计算机显示器)闪烁的频率高于约50Hz至60Hz时,主流研究把在此状态下的调制光认定为稳定状态。这种对调制光稳定感知,被称为闪烁融合阈值。然而,当调制光不均匀且包含图像时,所需要的闪烁融合阈值要高得多。如果人需要在一连串不同图像中识别指定图像,图像出现时间仅需13毫秒。视觉残留有时会导致非常短暂的单毫秒视觉刺激,其感知持续时间在100毫秒至400毫秒之间。多个短暂刺激有时也能合成单个刺激,例如在10毫秒的绿色闪光后出现10毫秒的红色闪光,会被感知为单一的黄色闪光。
--- wikipedia
首屏和流畅度
对于大部分场景来说,可能并没有一个明确要优化的对象,google 官方给出了 核心Web指标(Core Web Vitals)
首屏视图中最大的元素的渲染时间, 跟FCP区别,举一个特殊的场景,比如页面有全局的loading,loading文字已经绘制了内容。在FCP指标下是达成的,但这对于用户来说,并不是首屏。这种场景来说LCP才是更复合用户体验的首屏时间。
首次交互延迟,是从用户首次和网站进行交互到响应该事件的实际延时的时间。
页面产生的连续累计布局偏移分数, 主要是指异步数据加载导致的整体布局的变化。
与下一次绘制的交互, 用户交互(如点击或按键)后到下次在页面上看到视觉更新之间经过的时间
FID仅考虑用户与页面的首次交互,而且是仅计算Input Delay
, 也就是上图的灰色部分。INP 是计算所有的交互事件直到浏览器绘制下一帧(这也是名称由来)。这样的计算方式更符合用户的交互操作。所以chrome 最终用INP代替FID
Devtools是前端开发中使用频率最多的调试工具
Network面板是DevTools最常用的面板之一, 平时开发中可能看xhr接口请求,响应比较多。在性能分析的时候可以用来分析资源加载情况, 排队队列等情况。
Performance面板是我们最常用的性能分析工具。Performance基本能查看到性能指标的各项数据。
Netwwork (网络) 对应流畅度数据,
Frames(帧) 对应流畅度
数据,
Timings(计时) 对应FP
、FCP
、DCL
、LCP
、onLoad
Layout Shifts 对应CLS
Main
Lighthouse 是一种可帮助你提高网页质量的开源自动化工具, 他不仅包含了性能
,还会从更多维度来评估网页质量(无障碍, 最佳实践, SEO, PWA)。
其中性能
模块,也提供了上述性能指标的测试结果, 同时诊断结果也会提供优化建议。
Devtools 更适用于开发环境,在开发机上进行调试,但是开发机往往具有较高的硬件配置,以及稳定的软件运行环境,无法全面的反应网页在用户设备上性能状况。因此Performance API
有更广泛的适用场景,受限于用户设备的限制,无法适用Devtools来测试性能。Performance API 就可以很方便的帮我们记录并收集性能指标数据,为我们分析不同用户不同设备性能瓶颈提供数据支撑。
Performance API 主要包含以下模块检测
网络资源优化是一个比较常见,最容易见效的一个优化手段。
资源的压缩想在每个项目里面应该都有做,依托现代化的打包工具webpack
,esbuild
,rollup
等,都支持资源压缩。资源经过压缩后以及开启GZIP后,在网络传输中,传输时间会变短,从而加快页面加载速度。
最终的目的是减少资源体积。
减少请求数量和按需加载是一个平衡的过程。以webpack
为例,如果不进行配置,会将所有的静态资源都通过一个请求来获取,这样似乎满足了我们的要求,但是实际上,对于用户来说,这些资源之间是相互独立的,如果这些资源之间有依赖,那么就无法同时加载,从而导致页面加载时间变长。
因此,我们可以通过按需加载的方式,将一些依赖的资源,比如图片、css、js等,在用户需要时才进行加载,从而减少请求数量。
HTTP协议的历史就不在赘述,从我的视角来看,从业后基本就是HTTP 1.1的天下了。
HTTP/1.1
对于并发有一定的限制,一个连接只能发起一个请求,然后不同的浏览器对于并发的连接数有一定限制,比如我们常见的chrome是6个并发。因此我们单个资源的体积很小但是数量很大,加载资源时一定会排队造成阻塞。远古时代的雪碧图
, iconfont
,新时期的小图标base64编码
,symbol
等都一定程度上是解决这个问题的, 另外并发是针对域名的,所以还有一些开发者会使用讲资源部署到不同的域名下,从而规避这个问题。
SPDY
是Google 2010 年发布的,为了解决这个问题SPDY
支持了多路复用
,即单一的TCP连接可以同时发送多个请求(理论上无限制),从而解决了排队问题。除此外还有请求优先级
,压缩HTTP请求头
,服务器推送
等,Google 提出的目标是提升50%的web加载速度
HTTP/2
于2015.5正式发布,吸收了SPDY
, 主流浏览器于2015年底支持了该协议,所以有条件的建议大家升级为HTTP/2
HTTP/3 于2022/6 发布,优化了多路复用的队头阻塞问题,对于稳定性要求较高的可以再观望观望。
最快的网络优化就是使用缓存,直接加载缓存资源和数据。而缓存又分很多种。简单按照顺序谈一下缓存
DNS缓存是第一道缓存,DNS缓存是系统级别的。前端能做的DNS优化是 DNS Prefetch
, DNS Prefetch,即DNS预解析就是根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,来提高网站的访问速度。
浏览器查找缓存的策略如下图所示:
CDN的全称是Content Delivery Network,即内容分发网络,是互联网上用于加速网站、应用和视频 Delivery 的网络服务。网络的传输速度收到物理距离的影响较大,比如我们的网页部署在深圳,那么从北京访问速度会比从深圳访问要慢很多。CDN的作用就是将资源部署在不同区域的节点上,让用户就近访问,从而达到加速访问的目的。
距离说明就是北京用户访问到时候请求会先到CDN授权的服务器(一般指GSL,全局负载寻找区域), CDN服务器会根据用户IP,以及请求资源的URL, 让用户请求发送到最近的本地负载服务器(SLB),华北区域的服务器。本地负载服务器会让用户将请求发送到最近的资源服务器,北京的资源服务器会将资源(缓存)发送给用户。如果没有缓存,那么就会从源服务器(深圳)获取资源。
所以一般情况下第一次访问会比较慢(某个区域用户共用缓存),但是之后的访问就会很快了。
动态服务还有一个DCDN的方案,来做资源缓存,如果
nextjs
或者nuxtjs
相关cdn 需求的可以考虑做DCDN方案。
上面几个缓存主要讲的是资源的缓存,还会有一些相对固定的接口数据也可以缓存,避免每次去服务器请求。比如常见的省市区数据,行业数据等。服务器的缓存如redis
等,本文不做讨论。
本地数据缓存一般使用cookie
, localStorage
, sessionStorage
, IndexDB
。
我们一般缓存数据会比较大,所以一般会缓存到localStorage
或者IndexDB
, 这种大容量的持久缓存中。
离线资源缓存,是指在用户离线状态下,可以访问到本地的资源,比如图片、css、js等。
渐进式网页应用(Progressive Web App)
, 是一个比较好的实践。依赖于Service worker
, 我们可以离线使用。具体的说明可以参考MDN,当然在现代的框架下,开启pwa 是一个很简单的操作,比如说vite, 只需要安装vite-plugin-pwa
插件,简单的配置一下就可以开启。
当然pwa还是存在一定的兼容性问题。safari 11之后才支持。部分国产浏览器还不支持。如果对及时性要求较高,需要自定义完善的更新策略等。
混合应用(Hybrid App)
中,因为可以完全拦截请求,因此有一个离线包
的方案来做缓存,把所有的静态资源都下载到本地,然后拦截请求,返回本地的资源。当然完善的方案也包含版本更新,资源回退策略等。对于性能要求较高首屏甚至可以缓存接口数据,甚至可以实现离线状态下,正常渲染页面。
当浏览器开始接收到HTML的数据流时,主线程就开始解析HTML并且转换成DOM, 解析过程是流式进行的,浏览器在接收到一定量的HTML时解析就会开始,在解析HTML的同时,浏览器会开始请求页面中能够解析出的资源,如图片、CSS、JavaScript等。
解析时遇到内联或者同步的JavaScript,浏览器会停止解析HTML,执行JavaScript,执行完毕后继续解析HTML。
解析HTML时候遇到CSS,生成CSSOM,然后将DOM和CSSOM合并成一个渲染树。
除了让网页加载的快,还有一个就是让用户看起来加载的快,从用户感知层面,用户看到的内容越快越好。懒加载就是一种很好的方式, 首次只加载用户可视区域的内容, 这也是我们上面提到的目标中首屏
是一个很重要部分。
其原理也很简单,就是监听用户滚动事件,当用户滚动到某个区域时,再去加载这个区域的内容。
Dom 数量过多主要影响三点,一是加载速度(懒加载可以解决),二是渲染速度,三是css通用选择器存取大量引用。因此减少DOM数量是一个很好的优化手段。Lighthouse中 800个DOM节点是一个警告的阈值。1400个DOM节点是一个错误的阈值。 优化的方案请参考Avoid an excessive DOM size
减少无用的css(无用的也会解析),减少css文件大小(网络传输),减少css选择器的复杂度(文件大小, 解析时间),不要将样式应用于不需要的元素(* 选择器, 参考上条), 减少所有不必要的动画。
另外,尽管CSS 是阻塞渲染的资源, 但是如果cssDom不渲染,整个页面会是错乱的,因此需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间(将 CSS 放在 head 标签里)。
传统开发中,我们一般约定会将js放在body底部,这样可以避免js阻塞渲染。
但是现代的开发(spa)中,我们一般会将js放在head中,虽然js会阻塞渲染,但是js又是必须的,所以我们会将js放在head中,然后通过defer
或者async
来异步加载js。
更多是通过工程化的工具来进行代码分割
,懒加载
, 预加载和预取
等方案优化性能。
我们主要从运行时
来谈一下优化(部分)
减少Dom操作(Dom查询也属于Dom操作)。Dom操作是很耗时的,尽量减少Dom操作,比如我们可以将多次操作合并成一次操作,或者使用DocumentFragment
来减少Dom操作。目前的框架都会有虚拟Dom的概念,虚拟Dom的好处就是可以减少Dom操作,从而提高性能。
像我们平时讲的大部分框架层面的优化主要是为了这个目标,比如vue
的v-for
中的key
,除了减少Diff耗时,还有减少Dom操作。
避免内存泄露,比如事件绑定,定时器等,都会导致内存泄露,我们需要及时清除这些资源。
优化数据结构和算法,提升计算效率。比如我们可以使用Map
来代替Object
,使用Set
来代替Array
等。
通过web worker来进行计算密集型的操作,从而不阻塞主线程。可以参考Nextjs SSG 模式实现本地全文搜索中的实践。
使用requestAnimationFrame
来进行动画操作,这样可以保证动画的流畅性。
复用缓存,上面有讲过几种本地数据缓存机制,不在赘述。
有开源免费版本,支持Docker部署,其精美的UI界面,产品体验好,功能完善,是很多公司的首选。当然缺点就是需要付出不少的运营维护成本。
性能监控主要通过对 FCP
、LCP
、FID
、CLS
、FP
、TTFB
这些指标,做了不同维度的图表分析。
大厂一般会选择自建性能监控体系,不在此讨论之列。
阿里云出品,收费版本,接入简单。我们公司的整个监控体系就是基于arms来做的,主要是因为我们的业务部署在阿里云上,所以接入比较简单。支持全链路打通,后端改造的成本几乎没有。
缺点就是不开源,不支持私有化,功能相对单一。
性能监控主要包含页面加载时间详情
,页面加载瀑布图
, 性能分布
, 慢页面会话追踪
, 页面加载分布情况
等。
性能优化是一个很大的话题,本文只是简单的聊了一下,具体的优化手段还需要根据具体的业务场景来进行优化。性能优化是一个持续的过程,不是一蹴而就的,需要不断的去优化,去监控,去分析,去实践。
Find out how you stack up to new industry benchmarks for mobile page speed
Using site speed in web search ranking