本文将探讨浏览器渲染的 loading 过程,主要有 2 个目的:javascript
因为 loading 和 parsing 是相互交织、错综复杂的,这里面有大量的知识点,为了不过于发散本文将不会对每一个细节都深刻研究,而是将重点放在开发中容易控制的部分(Web 前端和 Web Server),同时因为浏览器种类繁多且不一样版本间差距很大,本文将侧重一些较新的浏览器特性php
提高页面性能方面已经有不少前人的优秀经验了,如 Best Practices for Speeding Up Your Web Site 和 Web Performance Best Practicescss
本文主要专一其中加载部分的优化,总结起来主要有如下几点:html
接下来就从浏览器各个部分的实现来梳理性能优化方法前端
首先是网络层部分,这方面的实现大部分是经过调用操做系统或 gui 框架提供的 apihtml5
为了应对 DNS 查询的延迟问题,一些新的浏览器会缓存或预解析 DNS,如当 Chrome 访问 google 页面的搜索结果时,它会取出连接中的域名进行预解析java
固然,Chrome 并非每次都将页面中的全部连接的域名都拿来预解析,为了既提高用户体验又不会对 DNS 形成太大负担,Chrome 作了不少细节的优化,如经过学习用户以前的行为来进行判断nginx
Chrome 在启动时还会预先解析用户常去的网站,具体能够参考 DNS Prefetching,当前 Chrome 中的 DNS 缓存状况能够经过 net-internals 页面来察看git
为了帮助浏览器更好地进行 DNS 的预解析,能够在 html 中加上如下这句标签来提示浏览器github
除此以外还可使用 HTTP header 中的 X-DNS-Prefetch-Control 来控制浏览器是否进行预解析,它有 on 和 off 两个值,更详细的信息请参考 Controlling DNS prefetching
本文不打算详细讨论这个话题,感兴趣的读者能够阅读 Content delivery network
在性能方面与此相关的一个问题是用户可能使用自定义的 DNS,如 OpenDNS 或 Google 的 8.8.8.8,须要注意对这种状况进行处理
因为 Web 页面加载是同步模型,这意味着浏览器在执行 js 操做时须要将后续 html 的加载和解析暂停,由于 js 中有可能会调用 document.write 来改变 dom 节点,不少浏览器除了 html 以外还会将 css 的加载暂停,由于 js 可能会获取 dom 节点的样式信息,这个暂停会致使页面展示速度变慢,为了应对这个问题,Mozilla 等浏览器会在执行 js 的同时简单解析后面的 html,提取出连接地址提早下载,注意这里仅是先下载内容,并不会开始解析和执行
这一行为还能够经过在页面中加入如下标签来提示浏览器
WebKit 也在尝试该功能,具体实现是在 HTMLLinkElement 的 process 成员函数中,它会调用 ResourceHandle::prepareForURL() 函数,目前从实现看它是仅仅用作 DNS 预解析的,和 Mozilla 对这个属性的处理不一致
对于不在当前页面中的连接,若是须要预下载后续内容能够用 js 来实现,请参考这篇文章 Preload CSS/JavaScript without execution
预下载后续内容还能作不少细致的优化,如在 Velocity China
2010 中,来自腾讯的黄希彤介绍了腾讯产品中使用的交叉预下载方案,利用空闲时间段的流量来预加载,这样即提高了用户访问后续页面的速度,又不会影响到高峰期的流量,值得借鉴
预渲染比预下载更进一步,不只仅下载页面,并且还会预先将它渲染出来,目前在 Chrome(9.0.597.0)中有实现,不过须要在 about:flags 中将’Web Page Prerendering’开启
不得不说 Chrome 的性能优化作得很细致,各方面都考虑到了,也难怪 Chrome 的速度很快
在网络层之上咱们主要关注的是 HTTP 协议,这里将主要讨论 1.1 版本,若是须要了解 1.0 和 1.1 的区别请参考Key Differences between HTTP/1.0 and HTTP/1.1
首先来看 http 中的 header 部分
header 的大小通常会有 500 多字节,cookie 内容较多的状况下甚至能够达到 1k 以上,而目前通常宽带都是上传速度慢过下载速度,因此若是小文件多时,甚至会出现页面性能瓶颈出在用户上传速度上的状况,因此缩小 header 体积是颇有必要的,尤为是对不须要 cookie 的静态文件上,最好将这些静态文件放到另外一个域名上
将静态文件放到另外一个域名上会出现的现象是,一旦静态文件的域名出现问题就会对页面加载形成严重影响,尤为是放到顶部的 js,若是它的加载受阻会致使页面展示长时间空白,因此对于流量大且内容简单的首页,最好使用内嵌的 js 和 css
header 中有些扩展属性能够用来保护站点,了解它们是有益处的
首先性能因素不该该是考虑使用 get 仍是 post 的主要缘由,首先关注的应该是否符合 HTTP 中标准中的约定,get 应该用作数据的获取而不是提交
之因此用 get 性能更好的缘由是有测试代表,即便数据很小,大部分浏览器(除了 Firefox)在使用 post 时也会发送两个 TCP 的 packet,因此性能上会有损失
在 HTTP/1.1 协议下,单个域名的最大链接数在 IE6 中是 2 个,而在其它浏览器中通常 4-8 个,而总体最大连接数在 30 左右
而在 HTTP/1.0 协议下,IE六、7 单个域名的最大连接数能够达到 4 个,在 Even Faster Web Sites 一书中的 11 章还推荐了对静态文件服务使用 HTTP/1.0 协议来提升 IE六、7 浏览器的速度
浏览器连接数的详细信息能够在 Browserscope 上查到
使用多个域名能够提升并发,但前提是每一个域名速度都是一样很快的,不然就会出现某个域名很慢会成为性能瓶颈的问题
主流浏览器都遵循 http 规范中的 Caching in HTTP 来实现的
从 HTTP cache 的角度来看,浏览器的请求分为 2 种类型:conditional requests 和 unconditional requests
unconditional 请求是当本地没有缓存或强制刷新时发的请求,web server 返回 200 的 heder,并将内容发送给浏览器
而 conditional 则是当本地有缓存时的请求,它有两种:
如下是 IE 发送 conditional requests 的条件,从 MSDN 上抄来
简单的来讲,点击刷新按钮或按下 F5 时会发出 conditional 请求, 而按下 ctrl 的同时点击刷新按钮或按下 F5 时会发出 unconditional 请求
须要进一步学习请阅读:
浏览器会尽量地优化前进后退,使得在前进后退时不须要从新渲染页面,就好像将当前页面先 “暂停” 了,后退时再从新运行这个 “暂停” 的页面
不过并非全部页面都能 “暂停” 的,如当页面中有函数监听 unload 事件时,因此若是页面中的连接是原窗口打开的,对于 unload 事件的监听会影响页面在前进后时的性能
在新版的 WebKit 里,在事件的对象中新增了一个 persisted 属性,能够用它来区分首次载入和经过后退键载入这两种不一样的状况,而在 Firefox 中可使用 pageshow 和 pagehide 这两个事件
unload 事件在浏览器的实现中有不少不肯定性因素,因此不该该用它来记录重要的事情,而是应该经过按期更新 cookie 或按期保存副本(如用户备份编辑文章到草稿中)等方式来解决问题
具体细节能够参考 WebKit 上的这 2 篇文章:
浏览器中对 cookie 的支持通常是网络层库来实现的,浏览器不须要关心,如 IE 使用的是 WinINET
须要注意 IE 对 cookie 的支持是基于 pre-RFC Netscape draft spec for cookies 的,和标准有些不一样,在设定 cookie 时会出现转义不全致使的问题,如在 ie 和 webkit 中会忽略 “=”,不过大部分 web 开发程序(如 php 语言)都会处理好,自行编写 http 交互时则须要注意
在 IE 中默认状况下 iframe 中的页面若是域名和当前页面不一样,iframe 中的页面是不会收到 cookie 的,这时须要经过设置 p3p 来解决,具体能够察看微软官方的文档,加上以下 header 便可
这对于用 iframe 嵌入到其它网站中的第三方应用很重要
页面的编码能够在 http header 或 meta 标签中指明,对于没有指明编码的页面,浏览器会根据是否设置了 auto detect 来进行编码识别(如在 chrome 中的 View-Encoding 菜单)
关于编码识别,Mozilla 开源了其中的 Mozilla Charset Detectors 模块,感兴趣的能够对其进行学习
建议在 http
header 中指定编码,若是是在 meta 中指定,浏览器在获得 html 页面后会首先读取一部份内容,进行简单的 meta 标签解析来得到页面编码,如 WebKit 代码中的 HTMLMetaCharsetParser.cpp,能够看出它的实现是查找 charset 属性的值,除了 WebKit 之外的其它浏览器也是相似的作法,这就是为什么 HTML5 中直接使用以下的写法浏览器都支持
须要注意不设定编码会致使不可预测的问题,应尽量作到明确指定
浏览器在加载 html 时,只要网络层返回一部分数据后就会开始解析,并下载其中的 js、图片,而不须要等到全部 html 都下载完成才开始,这就意味着若是能够分段将数据发送给浏览器,就能提升页面的性能,这就是 chunked 的做用,具体协议细节请参考 Chunked Transfer Coding
在具体实现上,php 中能够经过 flush 函数来实现,不过其中有很多须要注意的问题,如 php 的配置、web server、某些 IE 版本的问题等,具体请参考 php 文档及评论
注意这种方式只适用于 html 页面,对于 xml 类型的页面,因为 xml 的严格语法要求,浏览器只能等到 xml 所有下载完成后才会开始解析,这就意味着同等状况下,xml 类型的页面展示速度必然比 html 慢,因此不推荐使用 xml
即便不使用这种 http 传输方式,浏览器中 html 加载也是边下载边解析的,而不需等待全部 html 内容都下载完才开始,因此实际上 chunked 主要节省的是等待服务器响应的时间,由于这样能够作到服务器计算完一部分页面内容后就马上返回,而不是等到全部页面都计算都完成才返回,将操做并行
另外 Facebook 所使用的 BigPipe 其实是在应用层将页面分为了多个部分,从而作到了服务端和浏览器计算的并行
keepalive 使得在完成一个请求后能够不关闭 socket 链接,后续能够重复使用该链接发送请求,在 HTTP/1.0 和 HTTP/1.1 中都有支持,在 HTTP/1.1 中默认是打开的
keepalive 在浏览器中都会有超时时间,避免长期和服务器保持链接,如 IE 是 60 秒
另外须要注意的是若是使用阻塞 IO(如 apache),开启 keepalive 保持链接会很消耗资源,能够考虑使用 nginx、lighttpd 等其它 web server,具体请参考相关文档,这里就不展开描述
pipelining 是 HTTP/1.1 协议中的一个技术,能让多个 HTTP 请求同时经过一个 socket 传输,注意它和 keepalive 的区别,keepalive 能在一个 socket 中传输多个 HTTP,但这些 HTTP 请求都是串行的,而 pipelining 则是并行的
惋惜目前绝大部分浏览器在默认状况下都不支持,已知目前只有 opera 是默认支持的,加上不少网络代理对其支持很差致使容易出现各类问题,因此并无普遍应用
SPDY 是 google 提出的对 HTTP 协议的改进,主要是目的是提升加载速度,主要有几点:
从实现上看,frame 类(包括 iframe 和 frameset)的标签是最耗时的,并且会致使多一个请求,因此最好减小 frame 数量
若是要嵌入不信任的网站,可使用这个属性值来禁止页面中 js、ActiveX 的执行,能够参考 msdn 的文档
对于 html 的 script 标签,若是是外链的状况,如:
浏览器对它的处理主要有 2 部分:下载和执行
下载在有些浏览器中是并行的,有些浏览器中是串行的,如 IE八、Firefox三、Chrome2 都是串行下载的
执行在全部浏览器中默认都是阻塞的,当 js 在执行时不会进行 html 解析等其它操做,因此页面顶部的 js 不宜过大,由于那样将致使页面长时间空白,对于这些外链 js,有 2 个属性能够减小它们对页面加载的影响,分别是:
下图来自 Asynchronous and deferred JavaScript execution explained,清晰地解释了普通状况和这 2 种状况下的区别
须要注意的是这两个属性目前对于内嵌的 js 是无效的
而对于 dom 中建立的 script 标签在浏览器中则是异步的,以下所示:
为了解决 js 阻塞页面的问题,能够利用浏览器不认识的属性来先下载 js 后再执行,如 ControlJS 就是这样作的,它能提升页面的相应速度,不过须要注意处理在 js 未加载完时的显示效果
document.write 是不推荐的 api,对于标示有 async 或 defer 属性的 script 标签,使用它会致使不可预料的结果,除此以外还有如下场景是不该该使用它的:
简单来讲,document.write 只适合用在外链的 script 标签中,它最多见的场景是在广告中,因为广告可能包含大量 html,这时须要注意标签的闭合,若是写入的内容不少,为了不受到页面的影响,可使用相似 Google AdSense 的方式,经过建立 iframe 来放置广告,这样作还能减小广告中的 js 执行对当前页面性能的影响
另外,可使用 ADsafe 等方案来保证嵌入第三方广告的安全,请参考如何安全地嵌入第三方 js – FBML/caja/sandbox/ADsafe 简介
将 script 标签放底部能够提升页面展示给用户的速度,然而不少时候事情并没那么简单,如页面中的有些功能是依赖 js 的,因此更多的还须要根据实际需求进行调整
js 压缩可使用 YUI Compressor 或 Closure Compiler
gwt 中的 js 压缩还针对 gzip 进行了优化,进一步减少传输的体积,具体请阅读 On Reducing the Size of Compressed Javascript
比起 js 放底部,css 放页面顶部就比较容易作到
使用 @import 在 IE 下会因为 css 加载延后而致使页面展示比使用 link 标签慢,不过目前几乎没有人使用 @import,因此问题不大,具体细节请参考 don’t use @import
浏览器在构建 DOM 树的过程当中会同时构建 Render 树,咱们能够简单的认为浏览器在遇到每个 DOM 节点时,都会遍历全部 selector 来判断这个节点会被哪些 selector 影响到
不过实际上浏览器通常是从右至左来判断 selector 是否命中的,对于 ID、Class、Tag、Universal 和 Page 的规则是经过 hashmap 的方式来查找的,它们并不会遍历全部 selector,因此 selector 越精确越好,google page-speed 中的一篇文档 Use efficient CSS selectors 详细说明了如何优化 selector 的写法
另外一个比较好的方法是从架构层面进行优化,将页面不一样部分的模块和样式绑定,经过不一样组合的方式来生成页面,避免后续页面顶部的 css 只增不减,愈来愈复杂和混乱的问题,能够参考 Facebook 的静态文件管理
如下整理一些性能优化相关的工具及方法
以前提到的 http://www.browserscope.org 收集了各类浏览器参数的对比,如最大连接数等信息,方便参考
Navigation Timing 是还在草案中的获取页面性能数据 api,能方便页面进行性能优化的分析
传统的页面分析方法是经过 javascript 的时间来计算,没法获取页面在网络及渲染上所花的时间,使用 Navigation Timing 就能很好地解决这个问题,具体它能取到哪些数据能够经过下图了解(来自 w3c)
目前这个 api 较新,目前只在一些比较新的浏览器上有支持,如 Chrome、IE9,但也占用必定的市场份额了,能够如今就用起来
yahoo 开源的一个页面性能检测工具,它的原理是经过监听页面的 onbeforeunload 事件,而后设置一个 cookie,并在另外一个页面中设置 onload 事件,若是 cookie 中有设置且和页面的 refer 保持一致,则经过这两个事件的事件来衡量当前页面的加载时间
另外就是经过静态图片来衡量带宽和网络延迟,具体能够看 boomerang
本文出自 “百度技术博客” 博客,请务必保留此出处 http://baidutech.blog.51cto.com/4114344/746830