深入理解浏览器解析和执行过程

在我们公司的业务场景中,有很大一部分用户是使用老款安卓机浏览页面,这些老款安卓机性能较差,如果优化不足,页面的卡顿现象会更加明显,此时页面性能优化的重要性就凸显出现。优化页面的性能,需要对浏览器的渲染过程有深入的了解,针对浏览器的每一步环节进行优化。

页面高性能的判断标准是 60fps。这是因为目前大多数设备的屏幕刷新率为 60 次/秒,也就是 60fps , 如果刷新率降低,也就是说出现了掉帧, 对于用户来说,就是出现了卡顿的现象。

这就要求,页面每一帧的渲染时间仅为16毫秒 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有其他工作要做,因此这一帧所有工作需要在 10毫秒内完成。如果工作没有完成,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。

浏览器渲染流程

浏览器一开始会从网络层获取请求文档的内容,请求过来的数据是 Bytes,然后浏览器将其编译成HTML的代码。

但是我们写出来的HTML代码浏览器是看不懂的,所以需要进行解析。

渲染引擎解析 HTML 文档,将各个dom标签逐个转化成“DOM tree”上的 DOM 节点。同时也会解析内部和外部的css, 解析为CSSOM tree, css treedom tree结合在一起生成了render tree

render tree构建好之后,渲染引擎随后会经历一个layout的阶段: 计算出每一个节点应该出现在屏幕上的确切坐标。

之后的阶段被称为paiting阶段,渲染引擎会遍历render tree, 然后由用户界面后端层将每一个节点绘制出来。

最后一个阶段是 composite 阶段,这个阶段是合并图层。

浏览器内核

浏览器是一个极其复杂庞大的软件。常见的浏览器有chrome, firefox。firefox是完全开源,Chrome不开源,但Chromium项目是部分开源。

Chromium和Chrome之间的关系类似于尝鲜版和正式版的关系,Chromium会有很多新的不稳定的特性,待成熟稳定后会应用到Chrome。

浏览器功能有很多,包括网络、资源管理、网页浏览、多页面管理、插件和扩展、书签管理、历史记录管理、设置管理、下载管理、账户和同步、安全机制、隐私管理、外观主题、开发者工具等。

因此浏览器内部被划分为不同的模块。其中和页面渲染相关的,是下图中虚线框的部分渲染引擎。

渲染引擎的作用是将页面转变成可视化的图像结果。

目前,主流的渲染引擎包括TridentGeckoWebKit,它们分别是IE、火狐和Chrome的内核(2013年,Google宣布了Blink内核,它其实是从WebKit复制出去的),其中占有率最高的是 WebKit。

WebKit

最早,苹果公司和KDE开源社区产生了分歧,复制出一个开源的项目,就是WebKit。

WebKit被很多浏览器采用作为内核,其中就包括goole的chrome。

后来google公司又和苹果公司产生了分歧,google从webkit中复制出一个blink项目。

因此,blink内核和webkit内核没有特别的不同,因此很多老外会借用 chromium的实现来理解webkit的技术内幕,也是完全可以的。

浏览器源码

浏览器的代码非常的庞大,曾经有人尝试阅读Chromium项目的源码,git clone 到本地发现有10个G,光编译时间就3个小时(据说火狐浏览器编译需要更多的时间,大约为6个小时)。因此关于浏览器内部究竟是如何运作的,大部分的分享是浏览器厂商参与研发的内部员工。

国外有个非常有毅力的工程师Tali Garsiel 花费了n年的时间探究了浏览器的内幕,本文关于浏览器内部工作原理的介绍,主要整理自她的博客how browser work , 和其他人的一些分享。

国内关于浏览器技术内幕主要有《WebKit技术内幕》

下面,我们将针对浏览器渲染的环节,深入理解浏览器内核做了哪些事情,逐一的介绍如何去进行前端页面的优化。

浏览器渲染第一步:解析

解析是浏览器渲染引擎中第一个环节。我们先大致了解一下解析到底是怎么一回事。

什么是解析

通俗来讲,解析文档是指将文档转化成为有意义的结构,好让代码去使用他们

以上图为例,右边就是解析好的树状结构,这个结构就可以“喂“给其他的程序, 然后其他的程序就可以利用这个结构,生成一些计算的结果。

解析的过程可以分成两个子过程:lexical analysis(词法分析)syntax analysis(句法分析)

lexical analysis(词法分析)

lexical analysis 被称为词法分析的过程,有的文章也称为 tokenization,其实就是把输入的内容分为不同的tokens(标记),tokens是最小的组成部分,tokens就像是人类语言中的一堆词汇。比如说,我们对一句英文进行lexical analysis——“The quick brown fox jumps”,我们可以拿到以下的token:

  • “The”
  • “quick”
  • “brown”
  • “fox”
  • “jumps”

用来做lexical analysis的工具,被称为**lexer**, 它负责把输入的内容拆分为不同的tokens。不同的浏览器内核会选择不同的lexer , 比如说webkit 是使用Flex (Fast Lexer)作为lexer。

syntax analysis(句法分析)

syntax analysis是应用语言句法中的规则, 简单来说,就是判断一串tokens组成的句子是不是正确的。

如果我说:“我吃饭工作完了”, 这句话是不符合syntax analysis的,虽然里面的每一个token都是正确的,但是不符合语法规范。需要注意的是,符合语法正确 的句子不一定是符合语义正确的。比如说,“一个绿色的梦想沉沉的睡去了”,从语法的角度来讲,形容词 + 主语 + 副词 + 动词没有问题,但是语义上却是什么鬼。

负责syntax analysis工作的是**parser**,解析是一个不断往返的过程。

如下图所示,parserlexer要一个新的tokenlexer会返回一个token, parser拿到token之后,会尝试将这个token与某条语法规则进行匹配。

如果该token匹配上了语法规则,parser会将一个对应的节点添加到 parse tree (解析树,如果是html就是dom tree,如果是css就是 cssom tree)中,然后继续问parser要下一个node。

当然,也有可能该tokens没有匹配上语法规则,parser会将tokens暂时保存,然后继续问lexertokens, 直至找到可与所有内部存储的标记匹配的规则。如果找不到任何匹配规则,parser就会引发一个异常。这意味着文档无效,包含语法错误。

syntax analysis 的输出结果是parse tree, parse tree 的结构表示了句法结构。比如说我们输入"John hit the ball"作为一句话,那么 syntax analysis 的结果就是:

一旦我们拿到了parse tree, 还有最后一步工作没有做,那就是:translation,还有一些博客将这个过程成为 compilation / transpilation / interpretation

Lexicons 和 Syntaxes

上面提到了lexerparser 这两个用于解析工具,我们通常不会自己写,而是用现有的工具去生成。我们需要提供一个语言的 lexiconsyntaxes ,才可以生成相应的 lexerparser

webkit 使用的 lexer 和 parser 是 FlexBison

  1. flexcssflex 布局没有关系,是 fast-lexer 的简写,用来生成 lexer。 它需要一个lexicon,这个lexicon 是用一堆正则表达式来定义的 。
  2. bison 用来生成parsers, 它需要一个符合BNF范式的syntax。

lexicons

lexicons 是通过正则表达式被定义的,比如说,js中的保留字,就是lexicons 的一部分。

下面就是js中的保留字的正则表达式 的一部分。

/^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)*$/
复制代码

syntaxes

syntaxes 通常是被一个叫无上下文语法所定义,关于无上下文语法可以点击这个链接,反正只需要知道,无上下文语法要比常规的语法更复杂就好了。

BNF范式

非科班出身的前端可能不了解 BNF 范式(说的就是我 --),它是一种形式化符号来描述给定语言的语法。

它的内容大致为:

  1. 在双引号中的字("word")代表着这些字符本身。
  2. 而double_quote用来代表双引号。
  3. 在双引号外的字(有可能有下划线)代表着语法部分。
  4. 尖括号( < > )内包含的为必选项。
  5. 方括号( [ ] )内包含的为可选项。
  6. 大括号( { } )内包含的为可重复0至无数次的项。
  7. 竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
  8. ::= 是“被定义为”的意思。

下面是用BNF来定义的Java语言中的For语句的实例。

FOR_STATEMENT ::=
"for" "(" ( variable_declaration |
( expression ";" ) | ";" )
[ expression ] ";"
[ expression ]
")" statement
复制代码

BNF 的诞生还是挺有意思的一件事情, 有了BNF才有了真正意义上的计算机语言。巴科斯范式直到今天,仍然是个迷,巴科斯是如何想到的

小结

我们现在对解析过程有了一个大致的了解,总结成一张图就是这样:

对解析(parse)有了初步的了解之后,我们看一下HTML的解析过程。

解析HTML

HTML是不规范的,我们在写html的代码时候,比如说漏了一个闭合标签,浏览器也可以正常渲染没有问题的。这是一把双刃剑,我们可以很容易的编写html, 但是却给html的解析带来不少的麻烦,更详细的信息可以点击:链接

HTML lexicon

Html 的 lexicon 主要包括6个部分:

  • doctype
  • start tag
  • end tag
  • comment
  • character
  • End-of-file

当一个html文档被lexer 处理的时候,lexer 从文档中一个字符一个字符的读出来,并且使用 finite-state machine 来判断一个完整的token是否已经被完整的收到了。

HTML syntax

这里就是html 解析的复杂所在了。html 标签的容错性很高,需要上下文敏感的语法。

比如说对于下面两段代码:


<html lang="en-US">
  <head>
    <title>Valid HTMLtitle>
  head>
  <body>
    <p>This is a paragraph. <span>This is a span.span>p>
    <div>This is a div.div>
  body>
html>
复制代码
<html lAnG = EN-US>
<p>This is a paragraph. <span>This is a span. <div>This is a div.
复制代码

第一段是规范的html代码,第二段代码有非常多的错误,但是这两段代码在浏览器中都是大致相同的结构:

上面两处代码渲染出来的唯一的不同就是,正确的html会在头部有, 这行代码会触发浏览器的标准模式。

所以你看,html 的容错性是非常高的,这样是有代价的,这增加了解析的困难,让词法解析解析更加困难。

DOM Tree

HTML 解析出来的产物,经过加工,就得到了DOM Tree。

对于下面这种html的结构:


<html lang="en-US">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1">
  head>
  <body>
    <p>
      This is text in a paragraph.
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Rubber_Duck_%288374802487%29.jpg/220px-Rubber_Duck_%288374802487%29.jpg">
    p>
    <div>
      This is text in a div.
    div>
  body>
html>
复制代码

上面的html 的结构解析出来应该是:

说完了html的解析,我们就该说CSS的解析了。

解析CSS

和html 解析相比,css 的解析就简单很多了。

CSS lexicon

关于css的 lexicon, the W3C’s CSS2 Level 2 specification 中已经给出了。

CSS 中的 token 被列在了下面,下面的定义是采用了Lex风格的正则表达式。

IDENT	{ident}
ATKEYWORD	@{ident}
STRING	{string}
BAD_STRING	{badstring}
BAD_URI	{baduri}
BAD_COMMENT	{badcomment}
HASH	#{name}
NUMBER	{num}
PERCENTAGE	{num}%
DIMENSION	{num}{ident}
URI	url\({w}{string}{w}\)
|url\({w}([!#$%&*-\[\]-~]|{nonascii}|{escape})*{w}\)
UNICODE-RANGE	u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?
CDO	
:	:
;	;
{	\{
}	\}
(	\(
)	\)
[	\[
]	\]
S	[ \t\r\n\f]+
COMMENT	\/\*[^*]*\*+([^/*][^*]*\*+)*\/
FUNCTION	{ident}\(
INCLUDES	~=
DASHMATCH	|=
DELIM	any other character not matched by the above rules, and neither a single nor a double quote
复制代码

花括号里面的宏被定义成如下:

ident	[-]?{nmstart}{nmchar}*
name	{nmchar}+
nmstart	[_a-z]|{nonascii}|{escape}
nonascii	[^\0-\237]
unicode	\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
escape	{unicode}|\\[^\n\r\f0-9a-f]
nmchar	[_a-z0-9-]|{nonascii}|{escape}
num	[0-9]+|[0-9]*\.[0-9]+
string	{string1}|{string2}
string1	\"([^\n\r\f\\"]|\\{nl}|{escape})*\"
string2	\'([^\n\r\f\\']|\\{nl}|{escape})*\'
badstring	{badstring1}|{badstring2}
badstring1	\"([^\n\r\f\\"]|\\{nl}|{escape})*\\?
badstring2	\'([^\n\r\f\\']|\\{nl}|{escape})*\\?
badcomment	{badcomment1}|{badcomment2}
badcomment1	\/\*[^*]*\*+([^/*][^*]*\*+)*
badcomment2	\/\*[^*]*(\*+[^/*][^*]*)*
baduri	{baduri1}|{baduri2}|{baduri3}
baduri1	url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}
baduri2	url\({w}{string}{w}
baduri3	url\({w}{badstring}
nl	\n|\r\n|\r|\f
w	[ \t\r\n\f]*
复制代码

CSS Syntax

下面是css的 syntax 定义:

stylesheet  : [ CDO | CDC | S | statement ]*;
statement   : ruleset | at-rule;
at-rule     : ATKEYWORD S* any* [ block | ';' S* ];
block       : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
ruleset     : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
selector    : any+;
declaration : property S* ':' S* value;
property    : IDENT;
value       : [ any | block | ATKEYWORD S* ]+;
any         : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
              | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
              | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
              | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
              ] S*;
unused      : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
复制代码

CSSOM Tree

CSS解析得到的parse tree 经过加工之后,就得到了CSSOM TreeCSSOM 被称为“css 对象模型”。

CSSOM Tree 对外定义接口,可以通过js去获取和修改其中的内容。开发者可以通过document.styleSheets的接口获取到当前页面中所有的css样式表。

CSSOM

那么CSSOM 到底长什么样子呢,我们下面来看一下:


"en">

    
    
    "stylesheet" href="./test3.css">


    
"test1">TEST CSSOM1
"test2">TEST CSSOM2
"test3">TEST CSSOM3
复制代码

上面的代码在浏览器中打开,然后在控制台里面输入document.styleSheets,就可以打印出来CSSOM,如下图所示:

可以看到,CSSOM是一个对象,其中有三个属性,均是 CSSStylelSheet 对象。CSSStylelSheet 对象用于表示每个样式表。由于我们在document里面引入了一个外部样式表,和两个内联样式表,所以CSSOM对象中包含了3个CSSStylelSheet对象。

CSSStyleSheet

CSSStylelSheet对象又长什么样子呢?如下图所示:

CSSStyleSheet 对象主要包括下面的属性:

  • type

    字符串 “text/css”

  • href

    表示该样式表的来源,如果是内联样式,则href值为null

  • parentStyleSheet

    父节点的styleSheet

  • ownerNode

    该样式表所匹配到的DOM节点,如果没有则为空

  • ownerRule

    父亲节点的styleSheet中的样式对本节点的合并

  • media

    该样式表中相关联的 MediaList

  • title

    style 标签的title属性,不常见

    <style title="papaya whip">
      body { background: #ffefd5; }
    style>
    复制代码
  • disabled

    是否禁用该样式表,可通过js控制该属性,从而控制页面是否应用该样式表

样式表的解析

浏览器的渲染引擎是从上往下进行解析的。

当渲染引擎遇到