浏览器渲染原理
本文提到的浏览器特指 Chrome / Chromium

进程和线程
进程
程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为「 进程 」
每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意
线程
有了进程后,就可以运行程序的代码了, 运行代码的「基本单元」称之为「 线程 」
一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程。 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程
浏览器进程模型
当代 chrome 是多进程和多线程的模型, 线程是不能单独存在的,它是由进程来启动和管理的; 一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程
浏览器进程 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能
渲染进程 核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下
插件进程 主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
GPU 进程 其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程
NetworkService进程 主要负责页面的网络资源加载,网络进程内部会启动多个线程来处理不同的网络任务
实用程序AudioService进程 AudioService 是用于处理音频的,也并非是一定要使用的
实用程序StorageService进程 StorageService 是用于处理本地存贮的,包括 Storage (LocalStorage/SessionStorage)、Cache(CacheStorage/ApplicationCache)
其他实用程序进程 包括备用渲染程序进程、V8代理解析工具进程 等等
什么是沙箱
可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据,例如你的文档和桌面
Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。安全问题得以解决
渲染进程
Chromium默认进程策略,会为每个 Tab 标签创建一个渲染进程。 当渲染进程启动后,会开启一个渲染主线程,该主线程负责执行HTML
、CSS
、Javascript
渲染进程(Process
)主要包含的线程(Thread
)如下
渲染主线程
渲染主线程(Renderer Main Thread
)是浏览器中最繁忙的线程, 它通过事件循环
依次从消息队列中取出任务执行
主线程职责
需要主线程处理的任务包括但不限于以下内容
🔹 解析HTML
🔹 解析CSS
🔹 计算样式
🔹 布局
🔹 处理图层
🔹 每秒把页面画60次
🔹 执行全局JS代码
🔹 执行事件处理函数
🔹 执行计时器的回调函数
🔹 其他任务
主线程调度
要在单线程上处理这么多的任务,渲染主线程遇到了一个前所未有的难题, 那就是如何调度任务?
假设有如下场景 🔻
我正在执行一个JS函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
我正在执行一个JS函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
征对以上问题, 工程师为浏览器主线程而设计了大名鼎鼎的事件循环来处理多个任务, 步骤如下
INFO
🔹 在最开始的时候,渲染主线程会进入一个无限循环
🔹 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
🔹 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务 这样一来,就可以让每个任务有条不紊的、持续的进行下去了
异步的理解
如何理解Javscript
的异步?
Javscript
是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。 而渲染主线程承担着诸多的工作,渲染页面
、执行JS
都在其中运行。
如果使用同步的方式执行耗时等待任务,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。 这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现 象, 所以浏览器采用了异步的方式来执行耗时等待任务
具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身该立即结束该任务的执行,转而执行后续代码。
当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行
例如定时器任务执行过程如下: 主线程遇到setTimeout任务
, 会立即将任务发送给计时器线程
记录事件, 并结束该定时器任务
。
当计时完成,将原先的回调包装成任务添加到消息队列的末尾排队, 等待主线程的依次调用
概括
主线程是单线程, 且不允许被阻塞, 这就是异步产生的原因, 事件循环是异步的实现方式
消息循环
消息循环在Chrome
的规范中又称为事件循环
事件循环定义
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。在Chrome的源码中,它开启一个不会结束的for(;;)循环
,每次循环从消息队列中取出第一个任务执行,而其 他线程只需要在合适的时候将任务加入到队列未尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更 加灵活多变的处理方式。
根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的 队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器 必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行
消息队列线程
消息队列(Message Loop Thread
)里面包含了很多内部消息类型,如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。
除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。
队列的种类
Chrome
至少实现了以下消息队列
延时消息队列:
用于存放计时器到达后的回调任务,优先级「中」交互消息队列:
用于存放用户操作后产生的事件处理任务,优先级「高」微任务队列:
用户存放需要最快执行的任务,优先级「最高」其他任务队列
队列优先级
Chrome
浏览器中既然有多种消息队列, 且消息队列是有优先级的,那优先级顺序如何的呢?
每个任务都有一个
任务类型
, 同一类型的任务必须在一个队列
, 不同类型的任务可以分属于不同的队列浏览器必须准备好
一个微任务队列(Micro Task Queue)
, 微任务队列中的任务优先于
所有其他任务执行
注意: 宏任务概念来源
W3C规范文档#Event-Loop中,并没有提到宏任务这种类型的任务术语,只有任务队列(Task Queues
)中的任务(Task
)和微任务队列(Microtask Queue
)中的微任务(MicroTasks
)两种类型的任务。
宏任务(MacroTask
)概念出现在Chrome V8
的源码中, 主要用来区分微任务(Microtask
)。所以我们习惯性的将任务队列中的任务称之为宏任务,微任务队列中的任务称之为微任务
因此, 除开微任务队列, 将另外的任务队列视为普通消息队列, 在普通消息队列中全是一个个的宏任务, 宏任务执行过程中, 又会创建自己的微任务队列, 等待当前宏任务执行完成后, 接着开始执行微任务队列中的所有任务
IO 线程
IO 线程(IO Thread
)用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面的“处理其他线程发送的任务”一样了
合成器线程
合成器线程(Compositor Thread
)主要职责是
Input Handler
;Hit Tester
Web Content
中的滚动与动画- 计算
Web Content
的最优分层 - 协调图片解码、绘制、光栅化任务(
helpers
)
其中,Compositor thread helpers
的数目取决于 CPU 核心数
光栅线程
光栅线程(Raster Thread
)在Chromium渲染进程
中用于异步处理图形和文本的光栅化任务,以提高渲染性能和确保页面的响应性。这有助于实现流畅的Web页面渲染和动画效果
工作线程
工作线程(Worker Threads
、Service Worker
)是在渲染进程中创建的线程,用于执行一些耗时的 JavaScript
任务,以提高网页的响应性和性能,并允许在后台执行计算密集型或异步操作。这对于创建更流畅的Web应用程序和避免主线程的阻塞非常有帮助
共享渲染进程
Chrome渲染进程
的默认策略(process-per-site-instance
)是每个标签对应一个新的渲染进程,以保证不同的标签页之间不相互影响。
但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程,在下列场景下会共享相同的进程
同源的页面使用
a
标签打开, 同时设置target='_blank' rel='opener'
同源的页面使用
window.open
打开
如果想改变这种默认行为, 可以修改opener
页面使用
a
标签, 同时设置target='_blank' rel='noopener noreferrer'
页面使用
window.open(url, '', ‘noopener’)
以上可以通过笔者的demos代码进行测试, 打开Chrome
的任务管理器进行核对
如果想更加深入了解这块的内容, 请查看浏览器共享进程
渲染流程
网络交互
用户输入 URL 后, 浏览器进程判断是搜索还是输入的 URL, 合成完整的 URL
浏览器进程通过 IPC 将 URL 请求发送到网络进程
网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给 浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程
网络进程首先通过 DNS 的迭代查询, 得到最终的 IP 地址 (
浏览器DNS缓存
->操作系统DNS缓存
->路由器DNS缓存
->ISP中DNS缓存
->通过UDP向根域名服务器发送查询请求
->顶级域名服务器请求
->权限域名服务器请求
->得到最终的IP地址
)通过 IP 地址和服务器建立 TCP 链接, 再进行通信, 服务端发送响应内容到网络进程, 然后开始解析响应头 , 如果是 HTML, 则准备渲染进程, 主进程提交文档, 使渲染进程和网络进程之间建立通信管道
接下来进入渲染流水线
渲染流水线
当浏览器的网络进程收到HTML
文档后,会产生一个渲染任务,并推送给渲染进程的消息队列。
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
整个渲染流程分为多个阶段,分别是:HTML解析
、样式计算
、布局
、分层
、绘制
、分块
、光栅化
、画
每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。 这样,整个渲染流程就形成了一套组织严密的生产流水线。
第一步 解析HTML (Parse)
解析过程中遇到CSS解析CSS,遇到JS
执行JS
。为了提高解析效率,浏览器在开始解析前,会启动一个 预解析的线程,率先下载HTML
中的外部CSS文件和外部的JS
文件。
如果主线程解析到link
位置,此时外部的CSS
文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML
。这是因为下载和解析CSS
的工作是在预解析线程中进行的。这就是CSS
不会阻塞HTML解析的根本 原因。
如果主线程解析到script
位置,会停止解析HTML
, 转而等待JS文件下载好,并将全局代码解析执行完成 后,才能继续解析HTML
。这是因为JS代码的执行过程可能会修改当前的DOM树,所以D0M树的生成必 须暂停。这就是JS
会阻塞HTML解析的根本原因。
第一步完成后,会得到DOM树
和CSSOM(CSS Object Model)树
,浏览器的默认样式、内部样式、外部样式、行内样式均会包含 在CSSOM
树中
第二步 样式计算 (Style)
主线程会遍历得到的DOM
树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style
。
在这一过程中,很多预设值会变成绝对值,比如red
会变成rgb(255, 0, 0)
相对单位会变成绝对单位,比 如em会变成px
, 这一步完成后,会得到一棵带有样式的DOM
树
第三步 布局 (Layout)
接下来是布局,输入是DOM
树和计算的样式Computed Style
。输出是带有计算的布局信息的布局树(Layout Tree
)。在布局树上,每个元素都有精确的坐标和尺寸信息。
布局阶段会依次遍历DOM
树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位 置, 大部分时候,DOM
树和布局树并非一一对应。
比如display:none
的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择
器,虽然DOM
树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都 会导致DOM树和布局树无法一一对应
第四步 分层 (Layer)
下一步是分层, 主线程会使用一套复杂的策略对整个布局树中进行分层。
渲染器进程会为具有堆(层)叠上下文的属性的元素创建一个层。并非所有元素都有自己的图层。绘制图层的开销很大。为了获得更好的性能,渲染器进程仅在需要时才创建图层。
分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。 滚动条、堆叠上下文、transform
、opacity
等样式都会或多或少的影响分层结果,也可以通过will-change
属性更大程度的影响分层结果
注意
不要滥用will-change
来进行分层, 滥用会导致更加严重的渲染问题, 而不会提升效率
当元素频繁变动导致渲染,且这些元素未分为独立层,从而引发页面卡顿,出现效率问题的情况下使用
可以在chrome devtools
-> More tools
-> Layers
里面查看分层
第五步 绘制 (Paint)
再下一步是绘制
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。合成线程首先对每个图层进行分块,将其划分为更多的小区域。它会从线程池中拿取多个线程来完成分块(Tiling
)工作
注意
此刻, 渲染主线程工作到此为止, 剩余步骤交给其他线程来完成
第六步 光栅化 (Raster)
分块完成后,进入光栅化阶段。
合成线程会将块信息交给GPU进程,以极高的速度完成光栅化。GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。光栅化的结果,就是一块一块的位图
第七步 画 (Draw)
最后一个阶段就是画了
合成线程拿到每个层、每个块的位图后,生成一个个「DrawQuad
」信息。DrawQuad
会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。变形发生在合成线程,与渲染主线程无关,这就是transform
效率高的本质原因。合成线程会把DrawQuad
提交给浏览器GPU进程
,由浏览器GPU进程
产生系统调用,提交给GPU硬件
,完成最终的屏幕成 像。
整体小结
综上所述, 大致流程如下:
渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
创建布局树,并计算元素的布局信息。
对布局树进行分层,并生成分层树。
为每个图层生成绘制列表,并将其提交到合成线程。
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
合成线程发送绘制图块命令DrawQuad给浏览器进程。
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
重排和重绘
DOM解析阻塞
CSS
加载不会阻塞DOM
树的解析CSS
加载会阻塞DOM
树的生成CSS
加载会阻塞后面的JS
语句的执行JS
执行会阻塞DOM
树的解析和生成
强制更新布局
现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,将会触发浏览器同步的计算样式和布局
注意, 以下场景下均会触发同步计算样式和布局
第一类、元素类的APIs
元素API导致的同步布局
获取盒子尺寸
elem.offsetLeft
,elem.offsetTop
,elem.offsetWidth
,elem.offsetHeight
,elem.offsetParent
elem.clientLeft
,elem.clientTop
,elem.clientWidth
,elem.clientHeight
elem.getClientRects()
,elem.getBoundingClientRect()
滚动相关
elem.scrollBy()
,elem.scrollTo()
elem.scrollIntoView()
,elem.scrollIntoViewIfNeeded()
elem.scrollWidth
,elem.scrollHeight
elem.scrollLeft
,elem.scrollTop
等还包括设置它们的值
设置焦点类
elem.focus()
(chromium 源代码)
以及
elem.computedRole
,elem.computedName
elem.innerText
(chromium 源代码)
第二类、获取窗口尺寸
获取窗口导致的同步布局
window.scrollX
,window.scrollY
window.innerHeight
,window.innerWidth
window.visualViewport.height
/width
/offsetTop
/offsetLeft
(chromium 源代码)
第三类、document
document导致的同步布局
document.scrollingElement
only forces styledocument.elementFromPoint
第四类、表单元素的选中和聚焦
表单元素操作导致的同步布局
inputElem.focus()
inputElem.select()
,textareaElem.select()
第五类、鼠标事件
鼠标事件导致的同步布局
mouseEvt.layerX
,mouseEvt.layerY
,mouseEvt.offsetX
,mouseEvt.offsetY
(来源)
第六类、getCompatedStyle()
window.getComputedStyle()
通常会强制样式重新计算。
window.getComputedStyle()
通常也会强制布局。
以下三种情况之一均会强制布局
- 该元素位于
shadow tree
中 - 有媒体查询(与视口相关的查询)。具体来说,以下之一:(chromium 源码)
min-width
,min-height
,max-width
,max-height
,width
,height
aspect-ratio
,min-aspect-ratio
,max-aspect-ratio
device-pixel-ratio
,resolution
,orientation
,min-device-pixel-ratio
,max-device-pixel-ratio
- 请求的属性是以下之一:(chromium 源码)
height
,width
top
,right
,bottom
,left
- 当
margin
固定时, 获取margin
[-top
、-right
、-bottom
、-left
或它的复合属性] - 当
padding
固定时, 获取padding
[-top
、-right
、-bottom
、-left
或它的复合属性] transform
,transform-origin
,perspective-origin
translate
,rotate
,scale
grid
,grid-template
,grid-template-columns
,grid-template-rows
perspective-origin
- 这些项目以前曾在列表中,但似乎不再存在(截至 2018 年 2 月):,
motion-path
,motion-offset
,motion-rotation
,x
,y
,rx``ry
第七类、获取Range尺寸
获取Range尺寸导致的强制布局
range.getClientRects()
,range.getBoundingClientRect()
第八类、SVG导致的同步布局
有相当多的属性/方法,此列表只是一部分:
- SVG 可定位:
computeCTM()
,getBBox()
- SVG 文本内容:
getCharNumAtPosition()
,getComputedTextLength()
,getEndPositionOfChar()
,getExtentOfChar()
,getNumberOfChars()
,getRotationOfChar()
,getStartPositionOfChar()
,getSubStringLength()
,selectSubString()
- SVG使用:
instanceRoot
第九类、contenteditable
可编辑的内容元素导致的同步布局
- 复制图片到剪贴板
- 其他的操作还有很多chromium 源码
以上总结的列表都需要返回最新的布局信息,因此浏览器不得不清空队列,强制同步的(注意是同步, 非异步操作)触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。
重排 (Reflow)
浏览器一边生成渲染树, 一边计算每个元素最终的尺寸和位置。完成后, 页面中的所有元素的尺寸和位置就确定下来了, 即将被渲染到页面
- 获取元素的尺寸和位置
- 直接或间接改变元素的尺寸和位置(上述的强制更新布局)
重排的本质
重排的本质就是重新计算Layout
树
当进行了会影响布局树的操作后,需要重新计算布局树,会引发Layout
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当JS代码全部完成后再进行统一计算。所以,改动属性造成的Reflow
是异步完成的
也同样因为如此,当JS
获取布局属性时,就可能造成无法获取到最新的布局信息。浏览器在反复权衡下,最终决定获取属性立即Reflow
重绘 (Repaint)
浏览器一边Reflow
, 一边进行生成对应的图形绘制到页面, 绘制的过程称之为Repaint
。所有会导致Reflow
的代码, 均会导致Repaint
- 改变背景色
- 改变字体颜色
- 圆角边框
- 背景图
重绘的本质
重绘的本质就是重新根据分层信息计算了绘制指令。 当改动了可见样式后,就需要重新计算,会引发Repaint。由于元素的布局信息也属于可见样式,所以reflow一定会引起Repaint
匿名包含块
浏览器在渲染的过程中, 会遵循以下两个规则
- 文本内容必须在行盒中
- 行盒和块盒不能相邻
例如下面的代码, 文本1
必须在行盒中, 所以会生成匿名行盒, 文本2
包含块石行盒不能与块盒相邻, 所以会生成匿名块盒
<div>
<!-- 内部的文本会生成匿名行盒 -->
<div>文本1</div>
<!-- 此处会生成匿名块盒 -->
<span>文本2</span>
</div>
RIC 与 RAF
RequestIdleCallback(RIC)
与RequestAnimationFrame(RAF)
都是跟事件循环密切相关的API
,此段落我们来对比一下他俩的区别和使用场景
requestAnimationFrame
RAF
是浏览器提供的API
,用于安排在浏览器下一次重绘前运行的回调函数
在典型的事件循环中,当浏览器准备好渲染新的帧时,RAF
中注册的回调函数会在这个渲染前执行。RAF
通常与事件循环中的重绘(repaint
)和合成(composite
)阶段相关联
注意
它是在主线程中进行的, 执行时机是绘制阶段(
Paint
)前完成, 也就是浏览器在RAF
回调执行之后,会进行合成和绘制操作,将页面内容渲染到屏幕上这是下一帧的开始,一旦当前帧的绘制和合成完成,浏览器就开始准备下一帧的渲染
由于
RAF
的回调通常在下一次绘制前执行,它对于执行与渲染相关的任务(如动画)非常有用。RAF
可以确保你的JavaScript
逻辑在浏览器准备渲染下一帧之前执行,从而实现平滑的动画效果
requestAnimationFrame
与事件循环的联系:
受事件循环控制:
RAF
并不是由操作系统按照固定频率调用的。相反,它是在浏览器的事件循环中执行的,以确保回调函数在适当的时机与浏览器的渲染周期同步。RAF
会被浏览器用于确保在下一次绘制前执行动画或其他与渲染相关的任务调用时机:
RAF
的回调函数通常会在浏览器每秒渲染的帧数(通常是60帧每秒,即16.67毫秒一帧)附近调用。这个频率是理论上的最佳值,因为它与浏览器的刷新率相关。但实际的帧率可能受多种因素影响,包括设备性能、页面复杂度等
总的来说,requestAnimationFrame
通过与事件循环协同工作,确保动画和其他与渲染相关的任务在浏览器渲染周期内执行,以实现更加平滑和有效的动画效果。RAF
不是由操作系统直接调用的,而是由浏览器在合适的时机触发的
requestIdleCallback
RIC
是一种通过事件循环调度回调函数执行的机制,但它与普通的事件循环任务略有不同。它的目的是在浏览器空闲时执行任务,以便不干扰用户体验。当浏览器主线程空闲时,requestIdleCallback
会调用注册的回调函数
主线程空闲的一些常见条件
任务队列为空: 当主线程的任务队列为空时,即没有待处理的任务,浏览器可以认为主线程是空闲的。这包括
JavaScript
任务队列、样式计算、布局和绘制等。没有未完成的网络请求: 如果没有未完成的网络请求或其他异步操作,浏览器可能会认为主线程是空闲的。例如,所有的资源加载(如图片、脚本)都已完成。
没有未处理的用户交互: 当没有未处理的用户交互事件(例如点击、滚动等)需要处理时,浏览器也可能将主线程视为空闲。
动画和渲染完成: 如果当前的帧已经渲染完成,动画已经执行完毕,且没有等待下一帧的渲染任务,浏览器可能认为主线程是空闲的
requestIdleCallback
与事件循环的联系:
受事件循环控制:
RIC
是由事件循环控制的,但与一般任务不同,它是在浏览器认为“空闲”的时候才执行。这样设计的目的是避免影响用户交互、动画和其他时间关键的任务调用时机:
RIC
回调的调用时机不像requestAnimationFrame
那样在渲染周期内,而是在浏览器主线程空闲时,即没有更紧急的任务需要执行时。这通常是在事件循环的一次轮回中不是由操作系统调用: 与
requestAnimationFrame
一样,requestIdleCallback
也不是由操作系统直接调用的。它是由浏览器内部的事件循环调用的,以便更好地控制任务执行的时机
总的来说,requestIdleCallback
提供了一种机制,使开发者能够在浏览器空闲时执行一些非紧急的任务,以提高性能和用户体验。它是在事件循环中的一个特殊位置调度的,以确保任务的执行不会影响到对用户可见的操作
页面更新
在浏览器中, EventLoop
微任务执行完毕后, 此时会检查界面是否需要更新, 那浏览器到底如何更新界面呢?
页面更新步骤
- 当
Eventloop
执行完Microtasks
后,会判断document
是否需要更新,因为浏览器是60Hz
的刷新率,每16.6ms
才会更新一次。 - 然后判断是否有
resize
或者scroll
事件,有的话会去触发事件,所以resize
和scroll
事件也是至少16ms
才会触发一次,并且自带节流功能。 - 判断是否触发了
media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame
回调 - 执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback
回调
页面生命周期
浏览器中所有网页生命周期状态都是离散的且互斥的,这意味着网页一次只能处于一种状态。网页生命周期状态的大多数更改通常可通过 DOM 事件观察到
要说明网页生命周期状态以及用于指示这些状态之间转换的事件,最简单的方法可能是使用图表: