在现代 Web 开发中,了解浏览器如何渲染页面以及 JavaScript 如何影响页面加载流程可以更好地理解前端开发的核心原理。

一、浏览器渲染进程概述

浏览器的渲染进程(Render 进程)主要负责页面的渲染、脚本执行和事件处理等工作。为了避免因单个页面崩溃而导致整个浏览器崩溃,每个页面都有独立的渲染进程。

Render 进程是一个多线程架构,包含以下主要线程:

  1. GUI 渲染线程

    1. 负责渲染浏览器界面,包括解析 HTML 和 CSS,构建 DOM 树和 RenderObject 树,以及执行布局和绘制等操作。

    2. 当界面需要重绘(Repaint)或由于操作引发回流(Reflow)时,该线程会被激活。

    3. 注意:GUI 渲染线程与 JavaScript 引擎线程是互斥的。当 JavaScript 引擎执行时,GUI 线程会被挂起,所有需要更新的 GUI 操作会被暂存到一个队列中,只有当 JavaScript 引擎空闲时,这些操作才会被执行。

  2. JS 引擎线程

    1. 也称为 JavaScript 内核,如 V8 引擎,负责处理 JavaScript 脚本程序。

    2. 单线程特性 :JavaScript 主线程一次只能执行一个任务(同步代码),同步任务按顺序依次执行,不会阻塞其他 JavaScript 代码的执行。

    3. 任务队列(Task Queue) :异步任务的回调函数会被放置在这个队列中。有两种类型的任务队列:

      • 宏任务(macrotask)队列 :例如 setTimeoutsetInterval 的回调,网络请求的回调, Document Object 的事件等。

      • 微任务(microtask)队列 :由 PromiseMutationObserverprocess.nextTick(Node.js 环境) 等生成的回调。

    4. 事件循环(Event Loop)机制

      • 执行规则 :事件循环不断检测主线程是否空闲,如果空闲就从任务队列中取出任务执行。

      • 工作过程

        • 浏览器环境或 Node.js 环境中,JavaScript 引擎(如 V8)执行同步代码,完成主线程上的所有同步任务。

        • 如果遇到异步任务(如 setTimeoutsetInterval 的回调),它们会被暂时搁置在相应的任务队列中。

        • 引擎会监听宏任务和微任务队列中的任务:

          1. 微任务队列 的优先级高于 宏任务队列。在每次执行完主线程的一个同步任务之后,会先检查微任务队列,按顺序执行所有微任务,直到微任务队列空。

          2. 然后引擎会检查宏任务队列,取出一个任务执行。

  3. 事件触发线程

    1. 归属于浏览器而非 JavaScript 引擎,用于控制事件循环。

    2. 当执行诸如 Promise 等操作时,相关任务会被添加到事件线程中。当事件触发条件满足时,线程会将事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。

  4. 定时触发器线程

    1. 专门用于处理 setTimeoutsetInterval 等定时任务。

    2. 浏览器的定时计数器不依赖 JavaScript 引擎计数(因为 JavaScript 引擎是单线程的,若处于阻塞状态会影响计时的准确性)。当定时触发器线程计时完成后,会通知事件触发线程,将定时任务的回调函数添加到事件队列的队尾。

    3. 后台定时触发器线程计时的准确性问题:

      • 当浏览器处于后台时,为了节省系统资源和电量消耗,浏览器可能会对定时触发器线程的执行频率进行优化调整。将 setTimeoutsetInterval 的执行间隔延长,导致原本应该按照设定时间间隔执行的任务被延迟执行,从而出现计时不准确现象,如果有需要定时触发器线程要必须在后台执行强需求的话可以使用。

        • 开启JS多线程web weoker,倒计时写在weborker里时,页面的tab不会影响到倒计时的计算
          let webWorkDate = 100,
            date = 100;
          // 开启线程
          const work = new Worker('worker.js');
          setInterval(() => {
            date--;
            console.log('普通倒计数:', date);
          }, 1000);
          // 传输数据
          work.postMessage({ time: webWorkDate });
          console.log(work);
          // 监听线程
          work.onmessage = (event) => {
            console.log();
            console.log('Worker倒计数:', event.data.num);
            if (event.data.num === 0) {
              work.terminate(); //关闭线程
            }
          };
          //worker.js
          self.addEventListener(
            'message',
            function (e) {
              setInterval(() => {
                let num = e.data.time--;
                self.postMessage({ num });
              }, 1000);
            },
            false
          );
        
  5. 异步 HTTP 请求线程

    1. 在使用 XMLHttpRequestfetch 等进行网络请求时,浏览器会开启一个新的线程负责请求。

    2. 当检测到状态变更时,若设置有回调函数,异步线程会生成状态变更事件,并将回调函数放入事件队列中,等待 JavaScript 引擎执行。


二、页面加载的整体执行步骤

  1. 加载整体 HTML 文件

    1. 浏览器首先会加载整个 HTML 文件,解析其结构。
  2. 解析 HTML 并建立 DOM

    1. 浏览器会从上到下解析 HTML 文件,构建 DOM 树。在解析过程中,遇到诸如 <script><link> 等标签时,会下载和解析相应的内容。

    2. 如果是 <link> 标签,浏览器会解析 CSS 文件并构建 CSS 对象模型(CSSOM 树)。

  3. 结合 DOM 和 CSSOM 树生成 Render 树

    1. Render 树是 DOM 树和 CSSOM 树的结合体,用于描述页面中可见元素的布局和样式信息。
  4. 布局 Render 树(Layout/Reflow)

    1. 负责计算各元素的尺寸和位置等布局信息。
  5. 绘制 Render 树(Paint)

    1. 根据 Render 树中的信息,绘制页面的像素内容。
  6. GPU 合成

    1. 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成,并显示在屏幕上。

三、HTML、CSS 和 JavaScript 的解析与执行

  1. HTML 解析

    1. 浏览器从上到下解析 HTML 文件,构建 DOM 树。

    2. 遇到 <script> 标签时,若脚本是内部脚本,浏览器会立即解析并执行;若是外部脚本,浏览器会暂停解析 HTML,等待脚本下载完成后执行。

  2. CSS 解析

    1. CSS 有三种声明方式:外联样式表、内联样式表和内部样式表。浏览器会根据这些样式构建 CSSOM 树,用于渲染页面的样式。
  3. JavaScript 解析

    1. JavaScript 引擎负责解析和执行 JavaScript 脚本。执行 JavaScript 脚本时会阻塞 HTML 解析,因此建议将 <script> 标签放置在页面底部,以减少对页面加载的影响。

四、DOM 文档加载步骤

  1. 解析 HTML 结构:浏览器解析 HTML 文件,构建 DOM 树。

  2. 加载外部脚本和样式文件:加载外部的 JavaScript 和 CSS 文件。

  3. 解析并执行脚本代码:解析和执行 JavaScript 脚本。

  4. 执行事件绑定代码:如 $(function(){}) 中的代码。

  5. 加载二进制资源:加载图片等二进制资源。

  6. 页面加载完毕:执行 window.onload 事件。

希望本文能帮助你更深入地理解浏览器的工作原理,关于如何优化页面加载性能咱们下回接着再聊。