当你在浏览器输入 google.com 并且按下回车之后发生了什么?

下面摘录的是个人比较感兴趣的部分,从服务器响应到页面渲染。

HTTP 服务器请求处理

HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上的 Apache 和 nginx,以及 Windows 上的 IIS。

  • HTTPD 接收请求
  • 服务器把请求拆分为以下几个参数
    • HTTP 请求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或 TRACE)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法
    • 域名:google.com
    • 请求路径/页面:/ (我们没有请求 google.com 下的指定的页面,因此 / 是默认的路径)
  • 服务器验证其上已经配置了 google.com 的虚拟主机
  • 服务器验证 google.com 接受 GET 方法
  • 服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)
  • 如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 或 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写该请求
  • 服务器根据请求信息获取相应的响应内容,此处由于访问路径是 “/“,会访问首页文件(你可以重写该规则)
  • 服务器会使用指定的处理程序分析处理这个文件,假如 Google 使用 PHP,服务器会使用 PHP 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者

浏览器

浏览器背后的故事

当服务器提供了资源之后(HTML,CSS,JS,图片等),浏览器会执行下面的操作:

  • 解析 —— HTML,CSS,JS
  • 渲染 —— 构建 DOM 树 -> 渲染 -> 布局 -> 绘制

浏览器高层架构

组成浏览器的组件有:

  • 用户界面 用户界面包含了地址栏,前进后退按钮,书签菜单等等,除了请求页面之外所有你看到的内容都是用户界面的一部分
  • 浏览器引擎 浏览器引擎负责让 UI 和渲染引擎协调工作
  • 渲染引擎 渲染引擎负责展示请求内容。如果请求的内容是 HTML,渲染引擎会解析 HTML 和 CSS,然后将内容展示在屏幕上
  • 网络组件 网络组件负责网络调用,例如 HTTP 请求等,使用一个平台无关接口,下层是针对不同平台的具体实现
  • UI后端 UI 后端用于绘制基本 UI 组件,例如下拉列表框和窗口。UI 后端暴露一个统一的平台无关的接口,下层使用操作系统的 UI 方法实现
  • JavaScript 引擎 JavaScript 引擎用于解析和执行 JavaScript 代码
  • 数据存储 数据存储组件是一个持久层。浏览器可能需要在本地存储各种各样的数据,例如 Cookie 等。浏览器也需要支持诸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之类的存储机制

HTML 解析

浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成 8kB 大小的分块传输。

HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树。

解析树是以 DOM(Document Object Model)元素以及属性为节点的树。DOM 是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如 JavaScript)的接口。树的根部是 Document 对象。整个 DOM 和 HTML 文档几乎是一对一的关系。

解析算法

HTML 不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:

  • 语言本身的“宽容”特性
  • HTML 本身可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来支持它们
  • 解析过程需要反复执行。对于其他语言来说,源码不会在解析过程中发生变化,但是对于 HTML 来说,动态代码,例如脚本元素中包含的 document.write() 方法会在源码中添加内容,也就是说,解析过程实际上会改变输入的内容

由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。

解析结束之后

浏览器开始加载网页的外部资源(CSS,图像,JavaScript 文件等)。

此时浏览器把文档标记为可交互的(interactive),浏览器开始解析处于“推迟(deferred)”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成(complete)”,浏览器会触发“加载(load)”事件。

注意解析 HTML 网页时永远不会出现“无效语法(Invalid Syntax)”错误,浏览器会修复所有错误内容,然后继续解析。

页面渲染

  • 通过遍历 DOM 节点树创建一个“Frame 树”或“渲染树”,并计算每个节点的各个 CSS 样式值
  • 通过累加子节点的宽度,该节点的水平内边距(padding)、边框(border)和外边距(margin),自底向上的计算“Frame 树”中每个节点的首选(preferred)宽度
  • 通过自顶向下的给每个节点的子节点分配可行宽度,计算每个节点的实际宽度
  • 通过应用文字折行、累加子节点的高度和此节点的内边距(padding)、边框(border)和外边距(margin),自底向上的计算每个节点的高度
  • 使用上面的计算结果构建每个节点的坐标
  • 当存在元素使用 floated,位置有 absolutelyrelatively 属性的时候,会有更多复杂的计算,详见http://dev.w3.org/csswg/css2/http://www.w3.org/Style/CSS/current-work
  • 创建 layer(层)来表示页面中的哪些部分可以成组的被绘制,而不用被重新栅格化处理。每个帧对象都被分配给一个层
  • Textures are allocated for each layer of the page.
  • 每个层的帧对象都会被遍历,计算机执行绘图命令绘制各个层,此过程可能由 CPU 执行栅格化处理,或者直接通过 D2D/SkiaGL 在 GPU 上绘制
  • 上面所有步骤都可能利用到最近一次页面渲染时计算出来的各个值,这样可以减少不少计算量
  • 计算出各个层的最终位置,一组命令由 Direct3D/OpenGL 发出,GPU 命令缓冲区清空,命令传至 GPU 并异步渲染,帧被送到 Window Server

GPU 渲染

  • 在渲染过程中,图形处理层可能使用通用用途的 CPU,也可能使用图形处理器 GPU
  • 当使用 GPU 用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用 GPU 强大的并行计算能力,用于在渲染过程中进行大量的浮点计算

后期渲染与用户引发的处理

渲染结束后,浏览器根据某些时间机制运行 JavaScript 代码(例如 Google Doodle 动画)或与用户交互(在搜索栏输入关键字获得搜索建议)。类似 Flash 和 Java 的插件也会运行(尽管 Google 主页里没有)。这些脚本可以触发网络请求,也可能改变网页的内容和布局,触发新一轮的渲染与绘制。