看到这个标题大家一定会想到这篇神文《How Browsers Work》,这篇文章把浏览器的很多细节讲得很细,而且也被翻译成了中文。为什么我还想写一篇呢?因为两个原因, 1)这篇文章太长了,阅读成本太大,不能一口气读完。 2)花了大力气读了这篇文章后可以了解很多,但似乎对工作没什么帮助。 所以,我准备写下这篇文章来解决上述两个问题。希望你能在上班途中,或是坐马桶时就能读完,并能从中学会一些能用在工作上的东西。 浏览器工作大流程 废话少说,先来看个图: 从上面这个图中,我们可以看到那么几个事: 1)浏览器会解析三个东西:
2)解析完成后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。注意:
3)最后通过调用操作系统 Native GUI 的 API 绘制。 DOM 解析 HTML 的 DOM Tree 解析如下: <html> <head> <title>Web page parsing</title> </head> <body> <div> <h1>Web page parsing</h1> <p>This is an example Web page.</p> </div> </body> </html> 上面这段 HTML 会解析成这样: 下面是另一个有 SVG 标签的情况。 CSS 解析 CSS 的解析大概是下面这个样子(下面主要说的是 Gecko 也就是 Firefox 的玩法),假设我们有下面的 HTML 文档: <doc> <title>A few quotes</title> <para> Franklin said that <quote>"A penny saved is a penny earned."</quote> </para> <para> FDR said <quote>"We have nothing to fear but <span>fear itself.</span>"</quote> </para> </doc> 于是 DOM Tree 是这个样子: 然后我们的 CSS 文档是这样的: /* rule 1 */ doc { display: block; text-indent: 1em; } /* rule 2 */ title { display: block; font-size: 3em; } /* rule 3 */ para { display: block; } /* rule 4 */ [] { font-style: italic; } 于是我们的 CSS Rule Tree 会是这个样子: 注意,图中的第 4 条规则出现了两次,一次是独立的,一次是在规则 3 的子结点。所以,我们可以知道,建立 CSS Rule Tree 是需要比照着 DOM Tree 来的。CSS 匹配 DOM Tree 主要是从右到左解析 CSS 的 Selector,好多人以为这个事会比较快,其实并不一定。关键还看我们的 CSS 的 Selector 怎么写了。 注意:CSS 匹配 HTML 元素是一个相当复杂和有性能问题的事情。所以,你就会在N多地方看到很多人都告诉你,DOM 树要小,CSS 尽量用 id 和 class,千万不要过渡层叠下去,…… 通过这两个树,我们可以得到一个叫 Style Context Tree,也就是下面这样(把 CSS Rule 结点 Attach 到 DOM Tree 上): 所以,Firefox 基本上来说是通过 CSS 解析生成 CSS Rule Tree,然后,通过比对 DOM 生成 Style Context Tree,然后 Firefox 通过把 Style Context Tree 和其 Render Tree(Frame Tree)关联上,就完成了。注意:Render Tree 会把一些不可见的结点去除掉。而 Firefox 中所谓的 Frame 就是一个 DOM 结点,不要被其名字所迷惑了。 注:Webkit 不像 Firefox 要用两个树来干这个,Webkit 也有 Style 对象,它直接把这个 Style 对象存在了相应的 DOM 结点上了。 渲染 渲染的流程基本上如下(黄色的四个步骤):
注意:上图流程中有很多连接线,这表示了 Javascript 动态修改了 DOM 属性或是 CSS 属性会导致重新 Layout,有些改变不会,就是那些指到天上的箭头,比如,修改后的 CSS rule 没有被匹配到,等。 这里重要要说两个概念,一个是 Reflow,另一个是 Repaint。这两个不是一回事。
下面是一个打开 Wikipedia 时的 Layout/reflow 的视频(注:HTML 在初始化的时候也会做一次 reflow,叫 intial reflow),你可以感受一下: Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。 所以,下面这些动作有很大可能会是成本比较高的。
注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。 多说两句关于滚屏的事,通常来说,如果在滚屏的时候,我们的页面上的所有的像素都会跟着滚动,那么性能上没什么问题,因为我们的显卡对于这种把全屏像素往上往下移的算法是很快。但是如果你有一个 fixed 的背景图,或是有些 Element 不跟着滚动,有些 Elment 是动画,那么这个滚动的动作对于浏览器来说会是相当相当痛苦的一个过程。你可以看到很多这样的网页在滚动的时候性能有多差。因为滚屏也有可能会造成 reflow。 基本上来说,reflow 有如下的几个原因:
好了,我们来看一个示例吧: var bstyle = document.body.style; // cache bstyle.padding = "20px"; // reflow, repaint bstyle.border = "10px solid red"; // 再一次的 reflow 和 repaint bstyle.color = "blue"; // repaint bstyle.backgroundColor = "#fad"; // repaint bstyle.fontSize = "2em"; // reflow, repaint // new DOM element - reflow, repaint document.body.appendChild (document.createTextNode ('dude!')); 当然,我们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就 reflow 或 repaint 一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。但是有些情况浏览器是不会这么做的,比如:resize 窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行 reflow。 但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些 DOM 值:
因为,如果我们的程序需要这些值,那么浏览器需要返回最新的值,而这样一样会 flush 出去一些样式的改变,从而造成频繁的 reflow/repaint。 减少 reflow/repaint 下面是一些 Best Practices: 1)不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className。 // bad var left = 10, top = 10; el.style.left = left + "px"; el.style.top = top + "px"; // Good el.className += " theclassname"; // Good el.style.cssText += "; left: " + left + "px; top: " + top + "px;"; 2)把 DOM 离线后修改。如:
3)不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。 4)尽可能的修改层级比较低的 DOM。当然,改变层级比较底的 DOM 有可能会造成大面积的 reflow,但是也可能影响范围很小。 5)为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。 6)千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
几个工具和几篇文章 有时候,你会也许会发现在 IE 下,你不知道你修改了什么东西,结果 CPU 一下子就上去了到 100%,然后过了好几秒钟 repaint/reflow 才完成,这种事情以 IE 的年代时经常发生。所以,我们需要一些工具帮我们看看我们的代码里有没有什么不合适的东西。
最后,别忘了下面这几篇提高浏览器性能的文章:
参考
|