Better

Ethan的博客,欢迎访问交流

深入了解现代浏览器

看的 Inside look at modern web browse 系列文章的笔记,

计算机部件

了解一些计算机部件以及它们的作用。

  • CPU:Central Processing Unit,可以用来处理各种各样的工作
  • GPU:Graphics Processing Unit,不同于 CPU,CPU 擅长处理简单的任务,但同时跨越多个核
  • Process and Thread
    • Process 进程:可以理解为一个正在执行的程序
    • Thread 线程:驻留在进程内部,执行其进程程序的任何部分
    • 程序启动时,进程就被创建了,进程可能创建线程帮助它工作,但这是可选的。操作系统为进程分配一块内存,所有的应用状态都保存在这块私有内存中,当您关闭应用程序时,该进程也会消失,操作系统也会释放内存

浏览器架构

浏览器架构

  • Web 浏览器是如何使用进程和线程构建的呢?可能是一个进程有很多不同的线程,也可能是很多不同的进程有几个线程通过 IPC 通信。
  • Chrome 浏览器
    • Renderer Process:为每个选项卡创建一个渲染进程,这样的好处一个一个选项卡挂了,不会影响其他的。另一个好处就是安全和沙盒。由于每个进程有自己的私有内存空间,对于公共基础设施就会有多份拷贝,比如 V8,这意味着需要更多的内存,为了节省内存,Chrome 限制了它可以创建的进程数,这个限制取决于你设备的内存和 CPU 能力,当超过限制时,Chrome 会开始将多个选型卡运行在一个进程中。同样为了安全考虑,站点隔离是 Chrome 中最近引入的一个功能,它为每个跨站点的 iframe 运行一个单独的渲染进程
    • Browser Process
    • Utility Process
    • GPU Process
    • Plugin Process

你可以通过打开菜单 => 更多工具 => 任务管理器,查看当前进程列表,已经 CPU 和内存使用情况

浏览器导航

一次简单的导航,浏览器是怎么处理的呢

  • 处理输入:浏览器进程判断这是一个进程还是 URL
  • 开始导航:用户回车时,UI 线程初始化一个网络请求,在 Tab 上显示 spinner,网络线程通过适当的协议,如 DNS 查找和建立 TLS 连接。如果收到重定向结果,则一个新的 URL 请求会被初始化
  • 读取响应:当响应回来后,网络线程在必要时查看流的前几个字节,Content-Type 头会告诉 data 的数据类型,它可能丢失或错误。如果响应是 HTML 文件,将会把数据传给 Renderer Process,如果是 zip 或其他格式文件,意味着这是一个下载请求,需要将需求传给下载管理器。这也是安全检查发生的地方,如果域和响应数据似乎与已知的恶意站点相匹配,则网络线程发出警报,以显示警告页面。此外,为了确保敏感的跨站点数据不会进入渲染过程,还进行了跨源读取阻塞(CORB)检查。
  • 找到一个渲染器进程:所有的检查完成时,网络线程确信浏览器应该导航到所请求的站点,网络线程告诉 UI 线程数据已经准备好了。UI 线程然后找到一个渲染进程进行渲染的网页。
  • 提交导航:现在数据和渲染进程都准备好了,浏览器进程向渲染进程发送 IPC 以提交导航。当浏览器进程收到渲染进程中发生了提交的确认,导航就完成了,文档加载阶段就开始了。
  • 加载完成:渲染进程加载资源、渲染页面,当完成渲染时(所有的 onload 事件都触发且完成了执行),发送一个 IPC 给浏览器进程,这时候 UI 线程关闭 Tab 上的 spinner
  • 导航到其他站点:浏览器进程会再次执行之前的步骤,在这之前,它会检查当前的站点是否关心 beforeunload 事件

关于 Service worker

  • Service worker 是一种在应用程序代码中编写网络代理的方法;允许 web 开发人员对本地缓存的内容和何时从网络获取新数据有更多的控制。如果 service worker 设置为从缓存中加载页面,则不需要从网络请求数据。
  • Service worker 是 JavaScript 代码,运行在渲染进程中,那么当导航请求来时,浏览器进程如何知道站点有 Service worker 呢
  • 当 Service worker 被注册时,Service worker 的范围保存称为i一个引用,当导航发生时,网络线程根据注册的 service worker 作用域检查域,如果 service worker 已经注册了那个 URL,则 UI 线程找到渲染器进程去执行 service worker 代码

导航预加载

  • 如果 service worker 最终决定从网络上请求数据,那么这种浏览器进程和渲染进程的往返会导致延迟。
  • navigation preload 是一种机制,通过在 service worker 启动时并行加载资源来加速这个过程,它用一个头标记这些请求,允许服务器决定为这些请求发送不同的内容;例如,只更新了数据而不是完整的文档

渲染进程

渲染进程

  • 渲染进程负责 Tab 中所有的一切,主线程处理您发送给用户的大部分代码。如果使用了 web worker 或 service worker,则会交给 worker 线程处理
  • 合成和光栅线程也运行在渲染器进程内,以高效和平稳地呈现页面。
  • 解析阶段
    • 将 html 字符串解析成 DOM(Document Object Model)
    • 子资源加载:加载 image,css,js 等资源,主线程在解析 html 时,会请求相关的资源,但为了加速,预加载扫描器是并发运行的,如果发现有 img 或 link 标签,预加载扫描器查看 HTML 解析器生成的令牌,并将请求发送到浏览器进程中的网络线程。
    • javascript 会阻止解析:当发现 script 标签时,暂停解析 html 文档并加载,执行 javascript 代码,因为 javascript 可能改变文档的结构
  • 资源加载控制:async、defer、<link rel="preload">
  • 样式计算:主线程解析 CSS 并确定每个 DOM 节点的计算样式。
  • 布局:布局是一个找到元素几何形状的过程。主线程遍历 DOM 和计算样式,并创建布局树,其中包含 x/y 坐标和边界框大小等信息。布局树的结构可能与 DOM 树相似,但它只包含与页面上可见内容相关的信息。比如 display 为 none 则不在 layout tree 中,但 visibility 为 hidden 是在的。确定页面的布局是一项具有挑战性的任务。
  • 绘制
    • 拥有 DOM、样式和布局还不足以呈现一个页面。假设你想要复制一幅画。您知道元素的大小、形状和位置,但您仍然需要判断绘制它们的顺序。
    • 在这个绘制步骤中,主线程遍历布局树以创建绘制记录。绘画记录是绘画过程中“先有背景,再有文本,再有矩形”的一种记录。
    • 在渲染流水线中需要掌握的最重要的一点是,在每一步中,前一个操作的结果都用于创建新数据
    • 即使您的渲染操作与屏幕刷新保持同步,这些计算也是在主线路上运行的,这意味着当您的应用程序运行 JavaScript 时,这些计算可能会被阻塞。你可以将 JavaScript 操作分割成小块,并使用 requestAnimationFrame() 计划在每一帧运行。或者使用 Web Worker 去避免阻塞主线程
  • 合成
    • 将这些信息转化为屏幕上的像素称为光栅化。
    • 合成是一种技术,它将页面的各个部分分割成图层,分别栅格化,然后在称为合成线程的单独线程中合成为一个页面。
    • 如果发生滚动,因为图层已经栅格化,它所要做的就是合成一个新帧
    • 动画也可以通过移动图层和合成新帧来实现。
    • 划分层:为了找出哪些元素需要在哪些层中,主线程遍历布局树以创建层树,如果页面的某些部分应该是单独的层(如滑动侧菜单),你可以通过 will-change 属性提示浏览器,您可能会想给每个元素都添加图层,但是跨过多的图层组合可能会导致比每帧栅格化页面的一小部分更慢的操作,因此测量应用程序的渲染性能是至关重要的。
    • 一旦 layer tree 和绘制顺序被确定,主线程将该信息提交给组合线程,然后合成线程光栅化每个层,一个层可以像整个页面的长度一样大,因此,合成线程将它们划分为瓦片,并将每个瓦片发送给光栅线程。栅格线程栅格化每个贴图,并将它们存储在 GPU 内存中。合成线程可以对不同的栅格线程进行优先级排序,以便视图内(或附近)的东西可以先进行栅格排序。一旦瓦片光栅化完成,合成线程收集瓦片信息称为绘制四边形,创建一个合成帧。
    • 合成的好处是它不涉及主线程。合成线程不需要等待样式计算或 JavaScript 执行。这就是为什么合成动画被认为是最好的平滑性能。

输入事件

浏览器看输入事件

  • 浏览器进程将事件类型(如 touchstart)及其坐标发送给渲染进程。渲染进程通过找到事件目标并运行附加的事件监听器来适当的处理事件。
  • 如果页面上没有任何输入事件监听器,那么合成线程完全可以独立于主线程去创建一个新的合成帧。但页面上要是添加了事件监听呢?合成线程是怎么知道事件需要处理的呢?
  • 非快速滚动区:运行 JavaScript 代码是主线程的工作,所以在合成页面时,合成线程会将页面上具有事件处理程序的区域标记成“非快速滚动区域”。有了这个标记,合成线程就可以在该区域触发事件的时候将输入事件发送给主线程。如果输入事件不是这个区域的,那么合成线程就不会等待主线程,直接进行新帧的合成。
  • 事件代理是 Web 开发中常见的事件处理模式。这是利用了事件冒泡的机制,可以在最顶层的元素上添加事件处理程序,再根据事件目标进行处理。如果从浏览器的角度来看这段代码,整个页面都被标记成了非快速滚动区域。这也意味着,即使事件发生在你并不关注的区域里,合成线程也要去和主线程沟通,并等待回应。这样合成线程平滑滚动的优势就没有了。
  • 为了减轻这种情况的发生,你可以在事件监听里传递 passive: true。这表示你希望浏览器还是主线程来处理这个事件,并且合成线程同时可以继续合成新的帧。
  • 寻找事件目标:合成线程把事件发送给主线程以后,要做的第一件事情就是命中测试找到事件目标。命中测试就是使用渲染过程中产生的绘制记录来找出事件发生的点坐标下的元素。
  • 为了把对主线程的调用降到最低,Chrome 会合并连续触发的事件,比如 wheel、mousewheel、mousemove、pointermove、touchmove,并将它们延迟到下一次 requestAnimationFrame 之前发送。而所有的独立事件,比如 keydown, keyup, mouseup, mousedown, touchstart 以及 touchend 会立即发往主线程。
  • 合并事件在大部分情况下是可以保证很好的用户体验的,但是,在一些特殊场景下,比如你要开发的是一个绘图类的应用,就需要基于 touchmove 事件的坐标进行绘制,这时候合并事件就可能把区间内的坐标点丢失掉。这时候,你就可以使用目标事件的 getCoalescedEvents 来获取事件合并后的信息。

JavaScript 执行

优化 JavaScript 的执行

  • 对于动画效果的实现,避免使用 setTimeout 或 setInterval,请使用 requestAnimationFrame。
  • 将长时间运行的 JavaScript 从主线程移到 Web Worker。
    • 妥善处理 JavaScript 何时运行以及运行多久。例如,如果在滚动之类的动画中,最好是想办法使 JavaScript 保持在 3-4 毫秒的范围内。超过此范围,就可能要占用太多时间。如果在空闲期间,则可以不必那么斤斤计较所占的时间。
    • 在许多情况下,可以将纯计算工作移到 Web Worker
  • 使用微任务来执行对多个帧的 DOM 更改。
  • 使用 Chrome DevTools 的 Timeline 和 JavaScript 分析器来评估 JavaScript 的影响。

微任务简单示例

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
  var taskFinishTime;

  do {
    // Assume the next task is pushed onto a stack.
    var nextTask = taskList.pop();

    // Process nextTask.
    processTask(nextTask);

    // Go again if there’s enough time to do the next task.
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);

  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);

}

避免微优化 JavaScript:知道浏览器执行一个函数版本比另一个函数要快 100 倍可能会很酷,比如请求元素的 offsetTop 比计算getBoundingClientRect() 要快,但是,您在每帧调用这类函数的次数几乎总是很少,因此,把重点放在 JavaScript 性能的这个方面通常是白费劲。您一般只能节省零点几毫秒的时间。

坚持仅合成器的属性和管理层计数

  • 坚持使用 transform 和 opacity 属性更改来实现动画
  • 使用 will-change 或 translateZ 提升移动的元素:提前警示浏览器即将出现更改,根据您打算更改的元素,浏览器可能可以预先安排,如创建合成器层。
  • 避免过度使用提升规则;各层都需要内存和管理开销:如无必要,请勿提升元素。

相关资料



留言