前端性能精进之浏览器(四)——呈现

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 前端性能精进之浏览器(四)——呈现

 现如今,在呈现一个页面时,在浏览器中会打开众多进程,包括浏览器、渲染、插件、GPU、网络等进程。

  浏览器进程负责存储、界面、下载等管理。在渲染进程中,运行着熟知的主线程、合成线程、JavaScript 解释器、排版引擎等。

  而呈现一个页面大致可分为 4 个步骤:

  1. 浏览器进程处理用户在地址栏的输入,然后将 URL 发送给网络进程。
  2. 网络进程发送 URL 请求,在接收到响应数据后进行解析,接着转发给浏览器进程。
  3. 浏览器进程收到响应后,发送“提交导航”消息到渲染进程。
  4. 渲染进程开始接收网络进程发送的数据,并进行文档渲染。

  基于上述步骤可以联想到,呈现的优化分为两部分:资源和渲染。

  像上一节的图像其实也属于资源部分,只是内容比较多就单独创建了章节。

  本文所用的示例代码已上传至 Github


一、资源


  HTTP Archive 关于 2022 年页面大小的报告指出,按大小升序后,排在中间位置的移动页面大概有 70 个请求。

  包括 22 个图像、21 个脚本、7 个 CSS以及 2 个 HTML,脚本和 CSS 占了 40% 的请求。

  除了对这些资源进行尺寸优化之外,还可以对它们的加载进行优化。

1)优先级

  浏览器会给不同资源给予不同的请求优先级。

  以 Chrome 为例,分为多个等级,包括 Highest 、High、Low 和 Lowest 等,如下图所示。

  

  HTML 和 head 元素中的 CSS 优先级是最高的,head 元素中的脚本是高优先级,异步请求的脚本是低优先级。

  若优先级不符合预期,可以通过一些配置修改优先级,例如为 script 元素声明 async/defer,它的优先级就会变成低。

  在 img 元素中,新增了一个 fetchPriority 属性(如下所示),当值是 high 时,意味着这是一张重要的图像,浏览器会提升优先级立即开始请求。


img src="hero.png" fetchpriority="high" />

2)link 元素

  link 元素常用来加载 CSS 文件,但它还支持些其他功能,接下来会一一介绍。

  当 link 的 rel 属性值为 preload 时,就能预加载资源,如下所示。

<link rel="preload" href="demo.js" as="script" />

  as 属性是告知浏览器加载的资源类型,包括 style、script、font、image 等。

  预加载可提升资源的优先级,不过当资源在几秒后未使用时,浏览器会发出告警。

  当 link 的 rel 属性值为 preconnect 时,就能预连接站点,如下所示。

<link rel="preconnect" href="https://www.pwstrick.com" />

  另一个与连接相关的类型是 dns-prefetch(如下所示),用来处理 DNS 查询,即 DNS 预解析。

<link rel="dns-prefetch" href="https://www.pwstrick.com" />

  当 link 的 rel 属性值为 prefetch 时,就能预提取资源,如下所示。

<link rel="prefetch" href="demo.js" />

  预提取会让资源的优先级降为最低,用于让某些非关键资源提前请求,可为用户的下一步交互做准备。

  2023-03-23 当 link 的 rel 属性值为 prerender 时,就能预渲染指定的网站,如下所示。

<link rel="prerender" href="https://www.pwstrick.com" />

  不过,该参数的兼容性有限,Safari 和 Firefox 都不支持,如下图所示。

  

  有个名为 Tachyon 的开源库,基于 prerender,对页面之间的导航进行了提速。

  在用户将鼠标移动到链接时,会通过创建 link 元素,并赋予 prerender,实现指定地址的预渲染。

3)script 元素

  延迟(defer)和异步(async)的出现是为了解决 script 元素阻塞 HTML 解析的问题,下图描绘了 script 元素的 3 种运行机制。

  

  第一行是默认的运行机制,在解析HTML文档时,一遇到 script 元素就停止解析,改成下载外部脚本,然后执行脚本,执行完后才会继续解析。

  第二行是使用了 defer 属性后的运行机制,HTML 文档的解析和外部脚本的下载是同时进行的,解析完后才会执行脚本。

  第三行是使用了async 属性后的运行机制,HTML 文档的解析和外部脚本的下载也是同时进行,但下载完后就开始执行脚本,执行完后才会继续解析。

4)数据预请求

  在客户端的 WebView 中,每次请求后端接口大概要花 100~200ms,如果把这段时间省下来,那么也能减少白屏时间。

  数据预请求是将请求时机由业务发起提前到用户点击时,并行发送数据请求,缩短数据等待时间,如下图所示。

  

  这种改造需要客户端配合,现在简单介绍下我们公司当时实现的方案,流程图如下所示。

https://xxx.com/settings?path=game%2Fstrick&uid=xxxxx&refresh=1618451992

  

  首屏数据的接口信息,可以通过一些配置关联起来,比如一个单独的配置接口。

  客户端在拿到数据后,就会缓存到一个全局变量中,等待脚本读取。

  注意,到底是客户端先拿到数据,还是网页先拿到,这个无法确定,并且预请求只能以 get 方法通信。

  具体的实现方案如下:

  • 客户端分析出当前 URL 中的路径和参数,其中 refresh 参数(有的话)是一个时间戳(秒),这个参数用来控制客户端是否需要重新请求配置接口。
  • 当分析的 URL 参数中无 refresh 字段时,访问 https://xxx.com/settings 接口,并将URL路径、客户端默认带的参数(包含用户ID等)和 URL 本身的参数全部传递过来(如下所示),然后本地缓存。


  • 客户端会将 settings 接口的响应数据缓存到本地,而 key 就是当前 URL,也就是说 URL 不变的话,默认就不会去请求 settings 接口。若要穿透缓存,那么加上 refresh 参数,赋一个与之前不同的值即可。
  • settings 接口返回的 JSON 格式,包含 urls 字段(如下所示),是个数组,由接口集合组成,已经拼接好参数。
{
    "urls": [
        "http://xxx.com/xx/xx?id=2",
        "http://xxx.com/yy/yy?uid=1"
    ]
}
  • 客户端将读取到的数据注入到 WebView 的全局对象中,可以用全局变量同步读取,名字可自行约定,例如叫 TheLClientResponse,读取方式:window.TheLClientResponse,JSON 格式如下,其中 key 是 api 的路径,如果无数据可以返回 null。
{
    "xx/xx": {
        code: 0,
        msg: "test",
        data: {
            list: []
        }
    },
    "yy/yy": {
        code: 0,
        msg: "test",
        data: {
            list: []
        }
    }
}

5)字体

  CSS3 提供了 @font-face 规则允许为网页指定自定义字体,其声明和使用如下所示。

@font-face {
  font-family: "iconfont";
  src: url("../font/iconfont.woff2") format("woff2"),
    url("../font/iconfont.woff") format("woff"),
    url("../font/iconfont.ttf") format("truetype");
}
.iconfont {
  font-family: "iconfont";
}

  上述字体来源于 iconfont,为了兼容性考虑,往往会提供多个格式的字体。

  其中 ttf 是一种未压缩的格式,另外两种内部都做过压缩。在 2022 年大概有 75%~78% 的网页在使用 woff2 格式的字体。

  使用字体除了改变文字外形之外,还有一种普遍用法是用来显示 icon 小图标。

  CSS3 提供了 font-display 属性用于指定字体的渲染方式,在 @font-face 中声明,2022 年用的最多的值是 swap。

  swap 会让文字先按浏览器默认的字体展示,当字体加载完成后,再将其替换掉。在慢网中,会看到字体的前后变化。

  所以应该尽快加载字体,才能让用户享受到最优的体验。

  浏览器在解析 CSS 文件时,并不会马上下载 @font-face 中的字体文件。

  只有当发现 HTML 中有非空节点使用该字体时,才会开始下载。

  如果要提早下载,那么可以使用预加载,如下所示。

<link rel="preload" href="../../assets/font/dakai.woff2" as="font" crossorigin="anonymous"/>

  crossorigin 属性是必填的,表示允许跨域,若省略,就会有告警。

  还有一种优化方法是提取字体的子集(即有选择性的将需要的字符组合在一起),减小字体文件的尺寸,像图标就比较适合这样自定义。


二、渲染过程


  浏览器的渲染过程大致可分为 8 个阶段,如下图所示。

  

  下面的 1~5 步涉及主线程(main thread),6~8 步涉及合成线程(compositor thread)。

  1. 将 HTML 解析成 DOM 树,并将其存储在内存中,同时下载解析到的资源。
  2. 将 CSS 解析成样式表(style sheets),即生成 CSSOM,在此阶段会计算节点样式,并把相对的值和单位都转换成像素。
  3. 通过 DOM 和样式表生成布局树(layout tree),在此阶段会计算元素的尺寸和坐标,并且在树中不包含隐藏元素,但会包含 CSS 中创建的内容。
  4. 对布局树进行分层,生成分层树(layer tree),可控制绘画顺序,裁剪元素内容,CSS 中的 transform、z-index、will-change 等属性都与层相关。
  5. 通过布局树和分层树生成绘制列表,并将其提交给合成线程。
  6. 通过绘制列表和图层生成图块(tile),因为渲染所有图块会比较昂贵,所以会划分优先级,例如视口中的可见图块优先级会高。
  7. 图块在提交到光栅化(raster)线程池后,会被转移到 GPU 中,加速光栅化处理,即转换成位图(bitmap),最终结果会存储在 GPU 内存中。
  8. GPU 将位图传送回合成线程后,就会生成合成帧,处理完所有位图后,合成器线程向浏览器发送 Draw Quad 命令,开始在屏幕上显示页面。

  虽然这 8 个阶段的执行过程比较复杂,但是在现代浏览器中,它们会在 1/60 秒(即 16.67 毫秒)内完成,下图描述了整个渲染过程。

  

  优化渲染过程的核心就是缩短某个阶段的执行时间,或者直接跳过某些阶段。

1)流式渲染

  HTTP/1.1 协议支持分块传输编码(chunked transfer encoding),允许服务器将网页数据分成多块后再进行传输。

  在响应头中设置 Transfer-Encoding: chunked 就会启用分块传输编码的响应格式。

  浏览器在知道 HTML 会被流式返回后,就不用等到 HTML 下载完成后再开始解析了。

  不过,目前流行的客户端渲染(Client Side Render)其实并不需要专门的流式渲染,因为 HTML 的内容本来就少。

  若改成服务端渲染(Server Side Render),那就可根据实际情况进行流式渲染的优化了。

  具体的实现过程,本文不再赘述,可参考网上相关的方案,例如 Vue SSR 指南中的流式渲染

2)DOM

  HTML 在被解析时,一旦遇到 JavaScript,那么就会被阻塞,如下图所示。

  

  当遇到外部脚本时,还会停止 DOM 树的构建,转由网络进程去请求 JavaScript 脚本地址。

  CSS 本身并不会阻塞 DOM 树的构建,但在与 JavaScript 结合使用时,会出现阻塞。

  在下面的示例中,JavaScript 会修改 demo.css 文件中的样式。

<link rel="stylesheet" href="demo.css" />
<div id='root'>内容</div>
<script>
  const root = document.getElementById('root');
  root.style.color = 'red';
</script>

  主线程在执行脚本之前,需要先计算节点样式(即解析 CSS 文件),因此 DOM 树就无法被继续构建了。

  若要优化 DOM 树的构建,除了尽量避免上述不科学的写法之外,还可以从两方面入手:减少关键资源请求的数量和大小。

  所谓关键资源(key resource),更确切的说就是网页首屏的核心资源,没有它们,那么首屏将无法正确的呈现。

  减少资源的请求数量可以通过 2 个方法:

  • 将 CSS 或 JavaScript 内联到 HTML 结构中,例如移动端的屏幕适配脚本就比较适合内联。
  • 脚本元素可以增加 async 或 defer 的标记,具体可以参考上一节的 script 元素。

  关键资源的大小除了进行压缩外,就是只提取首屏需要的代码。

  将其他部分的代码合并到另一个文件,待需要时再加载,或者使用上一节所说的预提取。

3)重排和重绘

  重排(reflow)也叫回流,是指修改元素的几何属性后引起的重新渲染,涉及 7 个阶段,如下图所示,修改了元素的高度。

  

  触发重排的情况有添加或删除可见的元素、修改位置、边距或内容等。

  重绘(repaint)是指修改元素的背景颜色后引起的重新渲染,但与重排不同,重绘将直接进入 Paint 阶段,如下图所示。

  

  重排和重绘都会降低渲染性能,因为它们都发生在主线程中,并且布局、分层和绘制 3 个阶段的计算过程比较昂贵。

  当在脚本中获取元素的尺寸、位置等排版相关的信息时,就有可能触发强制重排,例如调用 offsetTop、clientWidth、getComputedStyle() 等属性或方法。

  优化它们的方式包括使用 cssText 或 CSS 类修一次性修改多个 CSS 属性,批量修改 DOM,例如使用文档片段 fragment、先隐藏元素再显示等。

  在众多的 CSS 属性中,有两个 CSS 属性(transform 和 opacity)可以避开重排和重绘,直接进入合成阶段。

  例如用 transform 属性实现的元素变化,就不会占用主线程,而是由合成线程处理,如下图所示。

  

  值得一提的是,早期在脚本中实现动画,都会借助定时器,但定时器无法精确的配置动画帧之间的时间间隔。

  按屏幕刷新率为每秒 60 次计算,那么理论上每帧的间隔约等于是 16.67 毫秒。

  但实际情况比较复杂,间隔不一定是这个值,有可能出现丢帧,从而造成动画不够平滑流畅。

  为了解决动画问题,浏览器提供了 requestAnimationFrame() 方法,在每一帧的开始执行配置的回调。

  注意,只有当浏览器 GPU 生成位图和屏幕显示位图保持同步时,才会触发 requestAnimationFrame() 的回调。

  在下面的示例中,让绝对定位的 span 元素通过 requestAnimationFrame() 向右偏移。

<span id='container' style="position:absolute">内容</span>
<script>
  let left = 0;
  const frame = () => {
    const container = document.getElementById('container');
    container.style.left = `${left++}px`;
    if (left > 100) return;
    requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
</script>

  注意,requestAnimationFrame() 也是运行在主线程中,如果主线程繁忙,那么也有可能延迟回调,造成动画的卡顿。

  并且如果其回调比较耗时(超过一帧),那么就会阻碍后续的任务。


总结


  本文的第一章节详细描述了资源的优化,并在开篇指出资源都存在着优先级,浏览器会按优先级进行请求。

  预加载可提升资源的优先级,预提取可降低资源的优先级,预连接可提前进行 TCP 连接或 DNS 查询。

  script 元素有延迟和异步两种运行机制,可有效地防止 HTML 解析的阻塞。

  数据预请求需要与客户端配合,本文给出了一份解决方案可供参考。

  自定义字体在页面开发中有着广泛的应用,常用的优化手段是预加载和减小尺寸。

  在第二章节中详细分析了浏览器的渲染过程,这个过程大致可分为 8 个阶段。

  围绕这些阶段,引出了流式渲染、DOM 树构建的优化。

  在重排和重绘中,详细说明了它们影响的阶段,并且列举了触发原因,以及优化手段。

  最后提到了合成动画,并且对比了 JavaScript 动画的两种实现方式。

 

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
2月前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
189 2
|
7月前
|
前端开发
调试前端时,在浏览器上修改参数并重新调用接口
有时候我们的页面点击过了,但是接口出问题,想修改参数再调用一次,一般是用apiPost工具把接口复制,再加上token和参数,但是这样非常的效率比较低。
826 0
|
4月前
|
存储 缓存 前端开发
前端谷歌浏览器面版属性
【8月更文挑战第19天】前端谷歌浏览器面版属性
55 0
|
4月前
|
Web App开发 监控 前端开发
前端必备浏览器调试工具
【8月更文挑战第19天】前端必备浏览器调试工具
94 0
|
1月前
|
前端开发 JavaScript API
前端开发的秘密花园:这些技巧让你轻松应对各种浏览器兼容性问题!
【10月更文挑战第31天】前端开发是一个充满创意与挑战的领域,追求极致用户体验的同时,浏览器兼容性问题却时常阻碍我们前进。本文将介绍几种解决浏览器兼容性的最佳实践:使用CSS前缀、Autoprefixer工具、现代JavaScript特性与Babel转译、Polyfill与Feature Detection、响应式设计以及跨域问题处理。掌握这些技巧,助你轻松应对各种兼容性难题,创建更稳定、用户友好的网页应用。
37 3
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
127 1
|
6月前
|
前端开发 安全 UED
【项目实战】从终端到浏览器:实现 ANSI 字体在前端页面的彩色展示
在学习和工作中,我们经常需要使用日志来记录程序的运行状态和调试信息。而为了更好地区分不同的日志等级,我们可以使用不同的颜色来呈现,使其更加醒目和易于阅读。 在下图运行结果中,我们使用了 colorlog 库来实现彩色日志输出。通过定义不同日志等级对应的颜色,我们可以在控制台中以彩色的方式显示日志信息。例如,DEBUG 级别的日志使用白色,INFO 级别的日志使用绿色,WARNING 级别的日志使用黄色,ERROR 级别的日志使用红色,CRITICAL 级别的日志使用蓝色。
|
1月前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
66 1
|
2月前
|
机器学习/深度学习 自然语言处理 前端开发
前端大模型入门:Transformer.js 和 Xenova-引领浏览器端的机器学习变革
除了调用API接口使用Transformer技术,你是否想过在浏览器中运行大模型?Xenova团队推出的Transformer.js,基于JavaScript,让开发者能在浏览器中本地加载和执行预训练模型,无需依赖服务器。该库利用WebAssembly和WebGPU技术,大幅提升性能,尤其适合隐私保护、离线应用和低延迟交互场景。无论是NLP任务还是实时文本生成,Transformer.js都提供了强大支持,成为构建浏览器AI应用的核心工具。
622 1
|
2月前
|
NoSQL 前端开发 MongoDB
前端的全栈之路Meteor篇(三):运行在浏览器端的NoSQL数据库副本-MiniMongo介绍及其前后端数据实时同步示例
MiniMongo 是 Meteor 框架中的客户端数据库组件,模拟了 MongoDB 的核心功能,允许前端开发者使用类似 MongoDB 的 API 进行数据操作。通过 Meteor 的数据同步机制,MiniMongo 与服务器端的 MongoDB 实现实时数据同步,确保数据一致性,支持发布/订阅模型和响应式数据源,适用于实时聊天、项目管理和协作工具等应用场景。