输入url到显示页面有多少地方可优化

前端性能优化

Posted by XIY on January 4, 2025

从输入 url 到展示前端页面开始聊起

明明已经被问烂了但面试官还是喜欢问的面试题,它究竟有什么魔力让面试官一直问?

面试官真的想知道从输入 url 到展示前端页面的整个流程吗?

其实可能更关注的是其中一些值得优化的点!!!

那么从整个流程看起,里面究竟有多少可以做性能优化的部分吧!

输入url后:DNS解析如何优化?

在我们输入url到渲染页面的过程中,其实有两类DNS解析。

  1. 页面DNS解析:当用户访问站点时,会进行页面的DNS解析,也就是解析我们输入的 url
  2. 其他DNS解析:如script,link的域名也要进行DNS解析,这个过程会阻拦到页面的主进程
  3. 页面DNS解析我们很难优化,因为你不能让用户没打开网站时进行解析。

但其他DNS解析却有手段进行,那就是 DNS 预解析(dns-prefetch)

它根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,进而提高网站的访问速度。

<link rel="dns-prefetch" href="//baidu.com">

拿到 ip 后:连接建立如何优化?

当我们通过 DNS 拿到 我们要请求的服务器地址后,如果是 http,就会有 3 次握手的时间,如果是 https 还要加上 SSL/TLS 连接的时间。

对于大部分请求来说,握手所占的时间可能会更长一些,真正的数据传输反而时间消耗要短些,那么这部分我们如何优化呢?

两个方向:

  1. 头部加入 Connection: Keep-Alive,开启管道,让其少建立连接
  2. 对其他的资源请求预建立连接,需要时直接通信 <link rel="preconnect" href="https://cdn.domain.com" as="style" crossorigin>

    preconnect 和 dns-prefetch 的区别是?

preconnect 包括 DNS 解析,TCP握手,TLS握手,而dns-prefetch 只包括 DNS解析的提前

拿到 HTML 后:DOM 树与 CSSOM 树解析如何优化?

当我们拿到 HTML 后,就会解析为 DOM 树和 CSSOM 树,因为浏览器不认识 HTML和 CSS,所以需要进行转换。

既然说要优化 DOM 树和 CSSOM 树解析,那接下来就该了解 DOM 树和 CSSOM 树的生成过程了!

HTML文件如何生成 DOM 树的?

  1. 字符流-> 词(token) 对于每个标签的开头如:<div>,标记为 StartTag;对标签的结尾</div>标记为 EndTag,文本标记为文本 Token
  2. 词 -> DOM 树 遇到 StartTag 生成DOM节点加入到栈中,遇到文件Token直接加入DOM树种,遇到EndTag,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div从栈中弹出,表示该 div 元素解析完成。

CSSOM 树是如何生成的?

  1. 数据预处理,对于一些相对单位需要分析出对应的精确值
  2. 样式继承:主要包含一下几类属性:字体类属性、文本类属性、visibility、list-style、cursor;
  3. 样式层叠:定义了 如何合并来自多个源的属性值的算法。

这一步解析到底有什么可以优化的点呢?

当 DOM 树和 CSSOM 树都渲染完成之后,就会合并生产渲染树。反过来说就是,只要有一颗没解析完成那么即使另一棵树解析完了也会卡住进度!

  1. DOM树解析慢: DOM 树在解析的过程中如果遇到 script 标签,会立刻停止解析去请求并执行 js,直到执行完了之后再继续解析!

我们可以给 script 标签设置 defer 或 async 去解决这个问题:

一般的script 标签流程是: DOM 解析暂停 -> script 下载 -> script 执行 -> DOM 解析继续

asnyc 的script 标签流程是: DOM 解析暂停 & script 下载 -> script 执行 -> DOM 解析继续

defer 的script 标签流程是: DOM 解析暂停 & script 下载 -> DOM 解析继续 -> script 执行

这样用户就不会等待太长时间了,白屏时间又缩短啦!

  1. CSSOM 树解析慢: 在实际情况中,常见 CSSOM 树解析会很慢的情况,比如加载第三方字体或图标等。

这时候可以通过 preload 进行优化! <link rel="preload" href="/style.css?t=2000" as="style">

preload 告诉浏览器立即加载资源,当资源被使用时就可以立即执行了;

这比 preconnect 还过分了,它好歹只是提前建立连接,preload直接不演了,不仅建立连接,还要立马把资源拿过来!

有了 preload 为什么需要 preconnect ?

既然 preload 可以把文件提前下好,那我为什么不都用 preload呢,还需要 preconnect?

这是因为浏览器对于请求的并发数有限制(chrome 为 6),非 HTTP2 下过多的请求会造成队列阻塞,同时过多的请求意味着逻辑的离散以及可能的数据处理时序。

一般来说首屏的请求数最好保持在 30 以内!!!

渲染树生成:与DOM树有什么不同?

DOM树可能包含一些不可见的元素,比如head标签,使用display:none;属性的元素等。所以在显示页面之前,还要额外地构建一棵只包含可见元素的渲染树。

如何结合DOM树和CSSOM树生成渲染树的?

在查找的过程中,出于效率的考虑,会从 CSSOM 树的叶子节点开始查找,对应在 CSS 选择器上也就是从选择器的最右侧向左查找。

除此之外,同一个 DOM 节点可能会匹配到多个 CSSOM 节点,而最终的效果由哪个 CSS 规则来确定,就是样式优先级的问题了。当一个 DOM 元素受到多条样式控制时,样式的优先级顺序如下:内联样式 > ID选择器 > 类选择器 > 标签选择器 > 通用选择器 > 继承样式 > 浏览器默认样式

  • 如果优先级相同,则最后出现的样式生效;
  • 继承得到的样式的优先级最低;

页面布局到页面绘制:如何优化浏览器的渲染效率?

页面布局的过程

通过计算渲染树上每个节点的样式,就能得出来每个元素所占空间的大小和位置。当有了所有元素的大小和位置后,就可以在浏览器的页面区域里去绘制元素的边框了。这个过程就是布局。

盒模型在布局过程中会计算出元素确切的大小和定位。计算完毕后,相应的信息被写回渲染树上,就形成了布局渲染树。

页面渲染1:构建图层

页面上可能有很多复杂的场景,比如3D变化、页面滚动、使用z-index进行z轴的排序等。所以,为了实现这些效果,渲染引擎还需要为特定的节点生成专用的图层。

在 Chorme 开发者工具的渲染下可以看到图层的情况;

渲染引擎给页面分了很多图层,这些图层会按照一定顺序叠加在一起,就形成了最终的页面。这里,将页面分解成多个图层的操作就成为分层, 最后将这些图层合并到一层的操作就成为合成。

什么样的节点才能让浏览器引擎为其创建一个新的图层呢?

  1. 拥有层叠上下文的元素

层叠上下文能够让页面具有三维的概念。这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。

  • 背景和边框:建立当前层叠上下文元素的背景和边框。
  • 负的z-index:当前层叠上下文中,z-index属性值为负的元素。
  • 块级盒:文档流内非行内级非定位后代元素。
  • 浮动盒:非定位浮动元素。
  • 行内盒:文档流内行内级非定位后代元素。
  • z-index:0:层叠级数为0的定位元素。
  • 正z-index:z-index属性值为正的定位元素。
  1. 需要裁剪的元素

假如有一个固定宽高的div盒子,而里面的文字较多超过了盒子的高度,这时就会产生裁剪,浏览器渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域。当出现裁剪时,浏览器的渲染引擎就会为文字部分单独创建一个图层,如果出现滚动条,那么滚动条也会被提升为单独的图层。

页面渲染2:绘制图层

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。

渲染引擎在绘制图层时,会把一个图层的绘制分成很多绘制指令,然后把这些指令按照顺序组成一个待绘制的列表

通常情况下,绘制一个元素需要执行多条绘制指令,因为每个元素的背景、边框等属性都需要单独的指令进行绘制。所以在图层绘制阶段,输出的内容就是绘制列表。

当图层绘制列表准备好之后,就会进行合成。

页面渲染3:合成

很多情况下,图层可能很大,比如一篇长文章,需要滚动很久才能到底,但是用户只能看到视口的内容,所以没必要把整个图层都绘制出来。因此,合成线程会将图层划分为图块,这些图块的大小通常是 256x256 或者 512x512。合成线程会优先将视口附近的图块生成位图。实际生成位图的操作是在光栅化阶段来执行的,所谓的光栅化就是按照绘制列表中的指令生成图片。 当所有的图块都被光栅化之后,合成线程就会生成一个绘制图块的命令,浏览器相关进程收到这个指令之后,就会将其页面内容绘制在内存中,最后将内存显示在屏幕上,这样就完成了页面的绘制。

浏览器做了这么多优化,我们还有什么地方可以优化呢?

这里不得不提到一个新的知识了。

浏览器的图层大概分为两类:一类是普通图层,一类是复合图层。

普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)。 其次,absolute 布局(fixed 也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层。然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)。

GPU 中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒。

开启复合图层单独加速

  • 最常用的方式:translate3d、translateZ。
  • opacity 属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)。
  • video iframe canvas webgl 等元素 开启复合图层和absolute这种方式的区别
  • absolute 虽然可以脱离普通文档流,但是无法脱离默认复合层。所以,就算 absolute 中信息改变时不会改变普通文档流中 render 树。但是,浏览器最终绘制时,是整个复合层绘制的。所以 absolute 中信息的改变,仍然会影响整个复合层的绘制。(浏览器会重绘它,如果复合层中内容多,absolute 带来的绘制信息变化过大,资源消耗是非常严重的)。
  • 硬件加速直接就是在另一个复合层了(另起炉灶),所以它的信息改变不会影响默认复合层(当然了,内部肯定会影响属于自己的复合层),仅仅是引发最后的合成(输出视图)。一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能。但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡。

回顾页面生成过程:浏览器进程在里面扮演的角色!!!

浏览器多进程架构:

  • 渲染进程
  • 浏览器主进程
  • 网络进程
  • GPU进程
  • 插件进程

每一个 Tab 页面打开时,都会生成一个渲染进程,这个进程下面包含诸多线程:

  1. GUI 线程:负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。与JS线程互斥;
  2. 事件触发线程:当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中,当 JS 引擎空闲时才会去执行;
  3. 定时器线程: setInterval 与 setTimeout 所在线程。计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行
  4. JS 引擎线程:JS 引擎线程负责解析 Javascript 脚本,运行代码。一个 Tab 页(渲染进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。与GUI线程互斥;
  5. 网络请求线程:在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求。将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

事件触发线程做了什么

  • JS 分为同步任务和异步任务。
  • 同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

这里就是常见的事件循环的知识了。

事件循环

  • 执行一个宏任务(栈中没有就从事件队列中获取)。
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)。
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染。
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)。

网络请求线程做了什么?

每次网络请求时都需要开辟单独的线程进行,譬如如果 URL 解析到 http 协议,就会新建一个网络线程去处理资源下载因此浏览器会根据解析出得协议,开辟一个网络线程,前往请求资源。 当我们输入 url 然后回车后,进程们做了什么?

  • 浏览器获取 url,浏览器主进程接管,开一个下载线程。
  • 然后进行 http 请求(略去 DNS 查询,IP 寻址等等操作),然后等待响应,获取内容。
  • 随后将内容转交给渲染进程,GUI 线程开始运行,浏览器渲染流程开始。

浏览器器内核的渲染进程拿到内容后,渲染过程大概可以划分成以下几个步骤:

  • 解析 html 建立 dom 树。
  • 解析 css 构建 render 树(将 CSS 代码解析成树形的数据结构,然后结合 DOM 合并成 render 树)。
  • 布局 render 树(Layout/reflow),负责各元素尺寸、位置的计算。
  • 绘制 render 树(paint),绘制页面像素信息。
  • 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(composite),显示在屏幕上。