从输入url到页面渲染?你还在这么回答吗

前言

现在正值秋招,金九银十已经到了银十的末尾,不知道各位25届的朋友是否已经有了心仪的offer,反正我是0offer,只能说非常惨淡。所以还是好好的在沉淀自己,从平常的点滴中慢慢积累,准备厚积薄发,这里用我最喜欢的艺人的一句话就是:那些看似不起波澜的日复一日,终会在未来的某一天让你看到坚持的意义!

相信大家面试肯定都会被问到这个问题,从输入url到页面渲染这个过程中会发生什么。很多人可能直接背的八股,就只有粗略的大概流程:

DNS解析 -> 建立tcp连接 -> 发送请求 -> 响应 -> html解析 -> css解析 -> 合成render树 -> 计算页面布局 -> 绘制 -> js执行

大概五分钟左右就讲完了,但其实里面的逻辑比这个要复杂很多,真的要讲的比较细其实是能讲15分钟左右的,而且还可以体现知识的深度。

前置知识

在了解浏览器的渲染流程之前,要先知道浏览器的基本架构。以Chrome为例:最上层是浏览器进程,负责协调承担各项工作的其他进程,比如实用程序进程、渲染器进程、GPU进程、插件进程等。

  • 浏览器进程:控制浏览器这个应用的chrome(主框架)部分,包括地址栏、书签、前进/后退按钮等,同时也会处理浏览器不可见的高权限任务,如发送网络请求、访问文件。
  • 渲染器进程:负责在标签页中显示网站及处理事件。
  • 插件进程:控制网站用到的所有插件。
  • GPU进程:在独立的进程中处理GPU任务。之所以放到独立的进程,是因为GPU要处理来自多个应用的请求,但要在同一个界面(显示器界面)上绘制图形。

当然,还有其他进程,比如扩展进程、实用程序进程。要知道你的Chrome当前打开了多少个进程,点击右上角的按钮,选择"更多工具",再选择"任务管理器"。

导航

从请求网页到浏览器准备渲染网页的过程,叫做导航。按照上面提到流程,也就是开始解析html之前的这个过程就叫做导航。导航涉及浏览器进程与线程间为显示网页而通信。一切从用户在浏览器中输入一个URL开始。输入URL之后,浏览器会通过互联网获取数据并显示网页。

  • 第一步:处理输入。UI线程会判断用户输入的是查询字符串还是URL。因为Chrome的地址栏同时也是搜索框。
  • 第二步:开始导航。如果输入的是URL,UI线程会通知网络线程发起网络调用,获取网站内容。此时标签页左端显示旋转图标,网络线程进行查询、建立TLS连接(对于HTTPS)。网络线程可能收到服务器的重定向头部,如HTTP 301。此时网络线程会跟UI线程沟通,告诉它服务器要求重定向。然后,再发起对另一个URL的请求。
  • 第三步:读取响应。服务器返回的响应体到来之后,网络线程会检查接收到的前几个字节。响应的Content-Type头部应该包含数据类型,如果没有这个字段,则需要MIME类型嗅探。如果响应是HTML文件,那下一步就是把数据交给渲染器进程。但如果是一个zip文件或其他文件,那就意味着是一个下载请求,需要把数据传给下载管理器。此时也是"安全浏览"检查的环节。如果域名和响应数据匹配已知的恶意网站,网络线程会显示警告页。此外,CORB检查也会执行,以确保敏感的跨站点数据不会发送给渲染器进程。
  • 第四步:联系渲染器进程。所有查检完毕,网络线程确认浏览器可以导航到用户请求的网站,于是会通知UI线程数据已经准备好了。UI线程会联系渲染器进程渲染网页。由于网络请求可能要花几百毫秒才能拿到响应,这里还会应用一个优化策略。第二步UI线程要求网络线程发送请求后,已经知道可能要导航到哪个网站去了。因此在发送网络请求的同时,UI线程会提前联系或并行启动一个渲染器进程。这样在网络线程收到数据后,就已经有渲染器进程原地待命了。如果发生了重定向,这个待命进程可能用不上,而是换作其他进程去处理。
  • 第五步:提交导航。数据和渲染器进程都有了,就可以通过IPC从浏览器进程向渲染器进程提交导航。渲染器进程也会同时接收到不间断的HTML数据流。当浏览器进程收到渲染器进程的确认消息后,导航完成,文档加载阶段开始。此时,地址栏会更新,安全指示图标和网站设置UI也会反映新页面的信息。当前标签页面的会话历史会更新,后退/前进按钮起作用。为便于标签页/会话在关闭标签页或窗口后恢复,会话历史会写入磁盘。
  • 最后一步:初始加载完成。提交导航之后,渲染器进程将负责加载资源和渲染页面(具体细节后面介绍)。而在"完成"渲染后(在所有iframe中的onload事件触发且执行完成后),渲染器进程会通过IPC给浏览器进程发送一个消息。此时,UI线程停止标签页上的旋转图标。

渲染

当导航结束之后,网络线程会去通知渲染器进程,告诉它可以开始渲染页面了,这个时候浏览器进程就要开始准备工作了。染是渲染器进程内部的工作,涉及Web性能的诸多方面,标签页中的一切都由渲染器进程负责处理。渲染的大致流程前面已经提过,就不再赘述。

html解析

因为浏览器无法直接理解和使用html,所以需要将html转换为浏览器能够理解的结构------DOM树。 在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。

  • 字节流转换为字符流: HTML文档以字节流的形式传输,浏览器首先将字节流转换为字符流。这通常涉及到字符编码的处理,例如UTF-8。

  • 词法分析(Lexical Analysis): 浏览器对字符流进行词法分析,将文档划分为一系列的标记(tokens)。标记是HTML文档中的最小单元,包括标签、属性、文本等。

  • 节点转换: 生成tokens后会去递归遍历这些token,浏览器会识别HTML标签,将它们转换为DOM节点(node),并确定它们之间的父子关系。这个过程也包括处理文本节点、属性等。

  • 语法分析(Syntax Analysis): 浏览器进行语法分析,根据HTML的语法规则将node组织成树状结构,即文档对象模型(DOM)树。

css解析

与HTML文本一样,渲染引擎也没法直接理解CSS文本,因此渲染引擎会将其转换为其能理解的结构------styleSheets。 css的解析规则和html解析几乎一样,只是最后生成的是css规则树。

针对styleSheets,结合CSS的继承、优先级层叠等规则,渲染引擎最终生成如下CSS规则树:

合成render树

现在DOM树和CSS规则树都有了,接下来就是将两者结合起来,对页面进行整体的布局。

DOM树只是描述了源码中HTML的结构,但其中许多元素并不需要展示在画面中,比如dispaly:none,也有一些不存在DOM树中但需要显示在页面上的元素(比如伪元素),因此在显示之前需要遍历DOM树中的所有节点,忽略掉不可见元素,添加不存在DOM树中但需要显示的的内容,最终生成一棵只包含可见元素的Render树。

计算页面布局

到这一步,渲染器进程知道了文档的结构,也知道了每个节点的样式。但基于这些信息仍然不足以渲染页面。比如,你通过电话跟朋友说:"画一个红色的大圆形,还有一个蓝色的小方形",你的朋友仍然不知道该画成什么样。

布局,就是要找到元素间的几何位置关系。这个过程非常复杂,确定页面的布局要考虑很多很多因素,并不简单。比如,字体大小、文本换行都会影响段落的形状,进而影响后续段落的布局。CSS可让元素浮动到一边、隐藏溢出边界的内容、改变文本显示方向。可想而知,布局阶段的任务是非常艰巨的。

这个视频提到了chorme布局的一些流程,感兴趣的可以看一看 www.youtube.com/watch?v=Y5X...

绘制

这个才是今天的重头戏,这里详细的描述了浏览器的渲染原理。有了DOM、样式和布局,仍然不足以渲染页面。还要解决先画什么后画什么,即绘制顺序的问题。渲染是一个流水线作业(pipeline):前一道工序的输出就是下一道工序的输入。这意味着如果布局树有变化,则相应的绘制记录也要重新生成。 如果元素有动画,浏览器就需要每帧运行一次渲染流水线。目前显示器的刷新率为每秒60次(60fps),也就是说每秒更新60帧,动画会显得很流畅。如果中间缺了帧,那页面看起来就会"闪眼睛"。

分层

为了更好的解决这些问题,浏览器的渲染引擎采用了分层机制。

每个DOM元素会有自己的布局信息Layout Object, 根据其布局信息的层级等关系,某些Layout Object会拥有共同的渲染层Paint Layer,某些Paint Layer又会拥有共同的合成层Composite Layer(Graphic Layers)。

如上图,DOM 树中得每个 Node 节点都有一个对应的 LayoutObject;拥有相同的坐标空间的 LayoutObjects,属于同一个渲染层(PaintLayer)。渲染层产生的最普遍条件是"层叠上下文 "。 这个是mdn对层叠上下文的解释:
developer.mozilla.org/zh-CN/docs/...

合成

知道各个元素的绘制顺序之后就可以开始绘制页面了,页面的绘制不是交给主线程去操作的,而是交给合成器线程。在页面的绘制过程中还会有一个栅格化的操作,把元素的样式信息转换为屏幕上的像素的这个过程就叫做栅格化。

如果直接一次性把这个页面全部绘制出来会产生很大的性能开销,所以合成器线程会先对页面进行分块,把当前出现在可视窗口的这些图块交给栅格化线程进行栅格化,转换成位图,然后合成器线程会将这些栅格化后的小图片进行合成,合成一个关键帧叫做合成器帧,然后把合成器帧交给GPU,由GPU进行处理最终显示在屏幕上。

交互

最后,我们看一看合成器如何处理用户交互。说到用户交互,有人可能只会想到在文本框里打字或点击鼠标。实际上,从浏览器的角度看,交互意味着来自用户的任何输入:鼠标滚轮转动、触摸屏幕、鼠标悬停等这些都是交互。

当用户交互比如触摸事件发生时,浏览器进程首先接收到该手势。但是,浏览器进程仅仅知道手势发生在哪里,因为标签页中的内容是渲染器进程处理。因此浏览器进程会把事件类型(如touchstart)及其坐标发送给渲染器进程。渲染器进程会处理这个事件,即根据事件目标来运行注册的监听程序。

具体来说,输入事件是由渲染器进程中的合成器线程处理的。如前所述,如果页面上没有注册事件监听程序,那合成器线程可以完全独立于主线程生成新的合成器帧。但是如果页面上注册了事件监听程序呢?此时合成器线程怎么知道是否有事件要处理?

这就涉及一个概念,叫"非快速滚动区"(non-fast scrollable region)。我们知道,运行JavaScript是主线程的活儿。在页面合成后,合成器线程会给附加了事件处理程序的页面区域打上"Non-Fast Scrollable Region"的记号。有了这个记号,合成器线程就可以在该区域发生事件时通过ITC通信,把事件发送给主线程。如果事件发生在这个区域外,那合成器线程会继续合成新帧而不会等待主线程。

这里有个值得注意的点,很多人喜欢使用事件委托来注册处理程序。这是利用事件冒泡原理,把事件注册到最外层元素上,然后再根据事件目标决定是否执行任务。一个事件处理程序就可以面向多个元素,这种高效的写法因此很流行。然而,从浏览器的角度来看,这样会导致整个页面被标记为"非快速滚动区"。这也就意味着,即便事件发生在那些不需要处理的元素上,合成器线程也要每次都跟主线程沟通,并等待它的回应。于是,合成器线程平滑滚动的优点就被抵消了。所以能不用这种写法就尽量不用。

总结

如果之前对浏览器的渲染原理不是很了解的小伙伴,看完这篇文章之后肯定会醍醐灌顶,仿佛七经八脉已经被打通了一脉。之前跨部门转正三面挂了,所以我的宇宙厂实习也到此结束了,失败了也不一定是坏事,最起码找到了自己的不足,后续还是要多去学习沉淀沉淀。不过当时给我提了一个意见就是觉得我的文章大部分都是偏应用,对知识理解的深度不够,所以后续我的文章会以原理为主,慢慢的也会去把之前的文章重新做个修改。

秋招即将结束,如果没拿到offer的小伙伴也别着急,有时候后退也是一种前进,人不是总要往高处走,人要往四处走,就像我一样,我也即将要往四处走走了。

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax