转:浏览器加载页面的过程与页面性能优化

  本文是转帖,原文:http://www.baiduux.com/blog/2011/02/15/browser-loading/
javascript

发布日期:2011年2月15日php

本文将探讨浏览器渲染的loading过程,主要有2个目的:css

  • 了解浏览器在loading过程当中的实现细节,具体都作了什么
  • 研究如何根据浏览器的实现原理进行优化,提高页面响应速度

因为loading和parsing是相互交织、错综复杂的,这里面有大量的知识点,为了不过于发散本文将不会对每一个细节都深刻研究,而是将重点 放在开发中容易控制的部分(Web前端和Web Server),同时因为浏览器种类繁多且不一样版本间差距很大,本文将侧重一些较新的浏览器特性html

现有知识

提高页面性能方面已经有不少前人的优秀经验了,如Best Practices for Speeding Up Your Web SiteWeb Performance Best Practices前端

本文主要专一其中加载部分的优化,总结起来主要有如下几点:html5

  • 带宽
    • 使用CDN
    • 压缩js、css,图片优化
  • HTTP优化
    • 减小转向
    • 减小请求数
    • 缓存
    • 尽早Flush
    • 使用gzip
    • 减小cookie
    • 使用GET
  • DNS优化
    • 减小域名解析时间
    • 增多域名提升并发
  • JavaScript
    • 放页面底部
    • defer/async
  • CSS
    • 放页面头部
    • 避免@import
  • 其它
    • 预加载

接下来就从浏览器各个部分的实现来梳理性能优化方法java

 

network

首先是网络层部分,这方面的实现大部分是经过调用操做系统或gui框架提供的apinginx

DNS

为了应对DNS查询的延迟问题,一些新的浏览器会缓存或预解析DNS,如当Chrome访问google页面的搜索结果时,它会取出连接中的域名进行预解析git

固然,Chrome并非每次都将页面中的全部连接的域名都拿来预解析,为了既提高用户体验又不会对DNS形成太大负担,Chrome作了不少细节的优化,如经过学习用户以前的行为来进行判断github

Chrome在启动时还会预先解析用户常去的网站,具体能够参考DNS Prefetching,当前Chrome中的DNS缓存状况能够经过net-internals页面来察看

为了帮助浏览器更好地进行DNS的预解析,能够在html中加上如下这句标签来提示浏览器

<link rel="dns-prefetch" href="//HOSTNAME.com">

除此以外还可使用HTTP header中的X-DNS-Prefetch-Control来控制浏览器是否进行预解析,它有on和off两个值,更详细的信息请参考Controlling DNS prefetching



DNS优化的原理和方法 


 

Yahoo和Google都有本身的建设高性能网站最佳实践, 我不作赘述, 须要了解的自行查阅资料:

上面的最佳实践条例其实也就是咱们常在YSlowPageSpeed这两个Firefox的add-ons中看到的网站检测结果的参考标准.

而整个WPO实际上是对浏览器(browser)的加载(load)和解析(parse)过程当中的一些消耗行为进行优化, 而load和parse在整个浏览器工做过程当中又互相纠结互相做用.

在这篇文字中讨论的更可能是FE们可以伸手处理或者经过达成共识的方法来进行快速推进Tech们协助的一些事情.

OK, 咱们慢慢把浏览器的工做过程掰细了看吧.

首先, 咱们先整一个浏览器如何找到一个网站的简易工做原理 – DNS查询:

首先当用户在浏览器的地址栏中敲入了网站的网址 ( 好比: alibaba.com ) ,这时浏览器会首先经过访问的域名来定位到IP (DNS) 从而找到去哪里获取资源, 这时, 浏览器会依次进行以下查找:

1. 浏览器缓存 :

浏览器首先会在本身的缓存中查找有没有对应的域名 – IP匹配, 若是好运的话, 这里就能够直接尝试去访问资源了, 若是运气平平则往下走吧.

2. 系统缓存 :

浏览器缓存中没有命中, 浏览器会告诉操做系统:”嘿, 我在我本身口袋里没找到, 可能丢了, 我得去你那看看”, 而后, 一个系统进程(?)调取系统中的DNS缓存进行查询, 重复上一条的运气判断…

3. 路由器缓存 :

走到这, 运气还真不太好啊, 操做系统也没辙了, 那怎么办呢, 向路由去要要看吧… 重复运气判断…

4. ISP DNS缓存 :

好吧, 真不知道说运气好仍是运气很差了, 不废话, 去ISP (网络提供商) 的DNS缓存服务器中寻找了, 通常状况下, 在ISP端的缓存中都能找到相应的缓存记录了, 不应这么背了, 或者… 您的ISP有够菜…

5. 递归搜索

最无奈的状况发生了, 在前面都没有办法命中的DNS缓存的状况下, ISP的DNS服务器开始从root域名服务器开始进行递归, 顺序是从.com顶级域名服务器到alibaba的域名服务器, 再没找到…好吧, 您认为您要去的网站真的公开存在么…?

要强调的是, 不仅是对网站第一次的域名访问须要作这样一次查询工做, 在对页面中的资源引用的域名解析时同样会有这样的一系列工做. 最明显的就是启用全新域名来作静态资源存储服务时, 基本上上述的1 – 5个步骤都得走上几遍. 才能让新域名在各DNS缓存服务器上留下记录.

在这个话题上, 关于DNS的相似系统级的解决方案不是FE可以控制得了的, 咱们q能够在涉及到DNS时有些小Tips来从中作些事情.

好吧, 第一项.DNS相关的优化:

常规实践 : DNS解析的复杂性决定了不当的使用多域名获取资源会形成没必要要的性能开销. 在WPO中, 不少优化工做是很艺术的, 在DNS和HTTP这两方面优化是就能够看到这个神奇的艺术性:

DNS的优化, 固然是尽量少的形成DNS查询开销, 而在HTTP优化的策略中有一项优化措施是避免单域名下链接数的缺陷来进行资源多通道下载, 实施的细节会在<HTTP优化的原理和方法> 中详细介绍, 在这里只是简单的提一下, 静态资源多域名服务能够绕过浏览器单域名载入资源时并行链接数的限制, DNS优化须要咱们尽量少的域名解析, HTTP优化时须要咱们适当的使用多域名服务, 那怎么样让两个优化实践都可以比较好的实施呢? [todo]

优雅降级 : 在某些现代浏览器 ( Google Chrome, Firefox 3.5+ ) 中, 已经可以支持DNS的预取了, 怎么个预取呢? 就是在浏览器加载网页时, 对网页中的<link>或者<a>的href属性中的域名进行后台的预解析(上文中的 1- 5步), 而且将解析结果缓存在浏览器端, 当用户在真正点击连接时, 省去在当下的DNS解析消耗, 把这个消耗过程转嫁到用户没法感知的浏览过程当中去.

第一, 现代浏览器已经支持且默认打开了DNS Prefetch的功能. 固然也能够经过浏览器的配置来管理该功能:

用Firefox3.5+能够这样: 浏览器默认就打开了HTTP协议下的DNS预取功能, 默认关闭HTTPS协议下的DNS预取功能, 可经过 about:config 的network.dns.disablePrefetchnetwork.dns.disablePrefetchFromHTTPS<两个选项来控制两种协议下的预取功能.

Chrome管理DNS Prefetch方法暂时缺乏.

第二, 能够经过用meta信息来告知浏览器, 我这页面要作DNS预取:

 

<meta http-equiv="x-dns-prefetch-control" content="on" />

第三,能够使用link标签来强制对DNS作预取:

 

 

<link rel="dns-prefetch" href="http://www.alibaba.com/" />

[todo DEMO]

扩展阅读:

另, 小康(lazyKang)同窗发现一个神奇的现象:

在一次无缓存访问中,  在一个并行下载通道内, 就算是同域名的状况, 也会形成DNS并行解析的消耗…

DNS预解析一次, 应该就能避免这样的问题, 空了作个DEMO试试看.

 



CDN

本文不打算详细讨论这个话题,感兴趣的读者能够阅读Content delivery network

在性能方面与此相关的一个问题是用户可能使用自定义的DNS,如OpenDNS或Google的8.8.8.8,须要注意对这种状况进行处理

link prefetch

因为Web页面加载是同步模型,这意味着浏览器在执行js操做时须要将后续html的加载和解析暂停,由于js中有可能会调用 document.write来改变dom节点,不少浏览器除了html以外还会将css的加载暂停,由于js可能会获取dom节点的样式信息,这个暂停 会致使页面展示速度变慢,为了应对这个问题,Mozilla等浏览器会在执行js的同时简单解析后面的html,提取出连接地址提早下载,注意这里仅是先 下载内容,并不会开始解析和执行

这一行为还能够经过在页面中加入如下标签来提示浏览器

<link rel="prefetch" href="http://">

但这种写法目前并无成为正式的标准,也只有Mozilla真正实现了该功能,能够看看Link prefetching FAQ

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

在网络层之上咱们主要关注的是HTTP协议,这里将主要讨论1.1版本,若是须要了解1.0和1.1的区别请参考Key Differences between HTTP/1.0 and HTTP/1.1

header

首先来看http中的header部分

header大小

header的大小通常会有500 多字节,cookie内容较多的状况下甚至能够达到1k以上,而目前通常宽带都是上传速度慢过下载速度,因此若是小文件多时,甚至会出现页面性能瓶颈出在 用户上传速度上的状况,因此缩小header体积是颇有必要的,尤为是对不须要cookie的静态文件上,最好将这些静态文件放到另外一个域名上

将静态文件放到另外一个域名上会出现的现象是,一旦静态文件的域名出现问题就会对页面加载形成严重影响,尤为是放到顶部的js,若是它的加载受阻会致使页面展示长时间空白,因此对于流量大且内容简单的首页,最好使用内嵌的js和css

header的扩展属性

header中有些扩展属性能够用来保护站点,了解它们是有益处的

  • X-Frame-Options
    • 这个属性能够避免网站被使用frame、iframe的方式嵌入,解决使用js判断会被var location;破解的问题,IE八、Firefox3.六、Chrome4以上的版本都支持
  • X-XSS-Protection
    • 这是IE8引入的扩展header,在默认状况下IE8会自动拦截明显的XSS攻击,如query中写script标签并在返回的内容中包含这项标签,若是须要禁止能够将它的值设为0,由于这个XSS过滤有可能致使问题,如IE8 XSS Filter Bug
  • X-Requested-With
    • 用来标识Ajax请求,大部分js框架都会加入这个header
  • X-Content-Type-Options
    • 若是是html内容的文件,即便用Content-Type: text/plain;的header,IE仍然会识别成html来显示,为了不它所带来的安全隐患,在IE8中能够经过在header中设置X- Content-Type-Options: nosniff来关闭它的自动识别功能

使用get请求来提升性能

首先性能因素不该该是考虑使用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上查到

使用多个域名能够提升并发,但前提是每一个域名速度都是一样很快的,不然就会出现某个域名很慢会成为性能瓶颈的问题

cache

主流浏览器都遵循http规范中的Caching in HTTP来实现的

从HTTP cache的角度来看,浏览器的请求分为2种类型:conditional requests 和 unconditional requests

unconditional请求是当本地没有缓存或强制刷新时发的请求,web server返回200的heder,并将内容发送给浏览器

而conditional则是当本地有缓存时的请求,它有两种:

  1. 使用了Expires或Cache-Control,若是本地版本没有过时,浏览器不会发出请求
  2. 若是过时了且使用了ETag 或Last-Modified,浏览器会发起conditional请求,附上If-Modified-Since或If-None-Match的 header,web server根据它来判断文件是否过时,若是没有过时就返回304的header(不返回内容),浏览器见到304后会直接使用本地缓存中的文件

如下是IE发送conditional requests的条件,从MSDN上抄来

  • The cached item is no longer fresh according to Cache-Control or Expires
  • The cached item was delivered with a VARY header
  • The containing page was navigated to via META REFRESH
  • JavaScript in the page called reload on the location object, passing TRUE
  • The request was for a cross-host HTTPS resource on browser startup
  • The user refreshed the page

简单的来讲,点击刷新按钮或按下F5时会发出conditional请求,而按下ctrl的同时点击刷新按钮或按下F5时会发出unconditional请求

须要进一步学习请阅读:

前进后退的处理

浏览器会尽量地优化前进后退,使得在前进后退时不须要从新渲染页面,就好像将当前页面先“暂停”了,后退时再从新运行这个“暂停”的页面

不过并非全部页面都能“暂停”的,如当页面中有函数监听unload事件时,因此若是页面中的连接是原窗口打开的,对于unload事件的监听会影响页面在前进后时的性能

在新版的WebKit里,在事件的对象中新增了一个persisted属性,能够用它来区分首次载入和经过后退键载入这两种不一样的状况,而在Firefox中可使用pageshow和pagehide这两个事件

unload事件在浏览器的实现中有不少不肯定性因素,因此不该该用它来记录重要的事情,而是应该经过按期更新cookie或按期保存副本(如用户备份编辑文章到草稿中)等方式来解决问题

具体细节能够参考WebKit上的这2篇文章:

cookie

浏览器中对cookie的支持通常是网络层库来实现的,浏览器不须要关心,如IE使用的是WinINET

须要注意IE对cookie的支持是基于pre-RFC Netscape draft spec for cookies的,和标准有些不一样,在设定cookie时会出现转义不全致使的问题,如在ie和webkit中会忽略“=”,不过大部分web开发程序(如php语言)都会处理好,自行编写http交互时则须要注意

p3p问题

在IE中默认状况下iframe中的页面若是域名和当前页面不一样,iframe中的页面是不会收到cookie的,这时须要经过设置p3p来解决,具体能够察看微软官方的文档,加上以下header便可

P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"

这对于用iframe嵌入到其它网站中的第三方应用很重要

编码识别

页面的编码能够在http header或meta标签中指明,对于没有指明编码的页面,浏览器会根据是否设置了auto detect来进行编码识别(如在chrome中的View-Encoding菜单)

关于编码识别,Mozilla开源了其中的Mozilla Charset Detectors模块,感兴趣的能够对其进行学习

建议在http
header中指定编码,若是是在meta中指定,浏览器在获得html页面后会首先读取一部份内容,进行简单的meta标签解析来得到页面编码,如WebKit代码中的HTMLMetaCharsetParser.cpp,能够看出它的实现是查找charset属性的值,除了WebKit之外的其它浏览器也是相似的作法,这就是为什么HTML5中直接使用以下的写法浏览器都支持

<meta charset="utf-8">

须要注意不设定编码会致使不可预测的问题,应尽量作到明确指定

chunked

浏览器在加载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

keepalive使得在完成一个请求后能够不关闭socket链接,后续能够重复使用该链接发送请求,在HTTP/1.0和HTTP/1.1中都有支持,在HTTP/1.1中默认是打开的

keepalive在浏览器中都会有超时时间,避免长期和服务器保持链接,如IE是60秒

另外须要注意的是若是使用阻塞IO(如apache),开启keepalive保持链接会很消耗资源,能够考虑使用nginx、lighttpd等其它web server,具体请参考相关文档,这里就不展开描述

pipelining

pipelining是HTTP/1.1协议中的一个技术,能让多个HTTP请求同时经过一个socket传输,注意它和keepalive的区 别,keepalive能在一个socket中传输多个HTTP,但这些HTTP请求都是串行的,而pipelining则是并行的

惋惜目前绝大部分浏览器在默认状况下都不支持,已知目前只有opera是默认支持的,加上不少网络代理对其支持很差致使容易出现各类问题,因此并无普遍应用

SPDY

SPDY是google提出的对HTTP协议的改进,主要是目的是提升加载速度,主要有几点:

  • Mutiplexed streams
    • 能够在一个TCP中传输各类数据,减小连接的耗时
  • Request prioritization
    • 请求分级,便于发送方定义哪些请求是重要的
  • HTTP header compression
    • header压缩,减小数据量

frame

从实现上看,frame类(包括iframe和frameset)的标签是最耗时的,并且会致使多一个请求,因此最好减小frame数量

resticted

若是要嵌入不信任的网站,可使用这个属性值来禁止页面中js、ActiveX的执行,能够参考msdn的文档

<iframe security="restricted" src=""></iframe>

javascript

加载

对于html的script标签,若是是外链的状况,如:

<script src="a.js"></script>

浏览器对它的处理主要有2部分:下载和执行

下载在有些浏览器中是并行的,有些浏览器中是串行的,如IE八、Firefox三、Chrome2都是串行下载的

执行在全部浏览器中默认都是阻塞的,当js在执行时不会进行html解析等其它操做,因此页面顶部的js不宜过大,由于那样将致使页面长时间空白,对于这些外链js,有2个属性能够减小它们对页面加载的影响,分别是:

  • async
    • 标识js是否异步执行,当有这个属性时则不阻塞当前页面的加载,并在js下载完后马上执行
    • 不能保证多个script标签的执行顺序
  • defer
    • 标示js是否延迟执行,当有这个属性时js的执行会推迟到页面解析完成以后
    • 能够保证多个script标签的执行顺序

下图来自Asynchronous and deferred JavaScript execution explained,清晰地解释了普通状况和这2种状况下的区别

须要注意的是这两个属性目前对于内嵌的js是无效的

而对于dom中建立的script标签在浏览器中则是异步的,以下所示:

var script = document.createElement('script');
script.src = 'a.js';
document.getElementsByTagName('head')[0].appendChild(script);

为了解决js阻塞页面的问题,能够利用浏览器不认识的属性来先下载js后再执行,如ControlJS就是这样作的,它能提升页面的相应速度,不过须要注意处理在js未加载完时的显示效果

document.write

document.write是不推荐的api,对于标示有async或defer属性的script标签,使用它会致使不可预料的结果,除此以外还有如下场景是不该该使用它的:

  • 使用document.createElement建立的script
  • 事件触发的函数中,如onclick
  • setTimeout/setInterval

简单来讲,document.write只适合用在外链的script标签中,它最多见的场景是在广告中,因为广告可能包含大量html,这时须要注意标签的闭合,若是写入的内容不少,为了不受到页面的影响,可使用相似Google AdSense的方式,经过建立iframe来放置广告,这样作还能减小广告中的js执行对当前页面性能的影响

另外,可使用ADsafe等方案来保证嵌入第三方广告的安全,请参考如何安全地嵌入第三方js – FBML/caja/sandbox/ADsafe简介

script标签放底部

将script标签放底部能够提升页面展示给用户的速度,然而不少时候事情并没那么简单,如页面中的有些功能是依赖js的,因此更多的还须要根据实际需求进行调整

  • 尝试用Doloto分析出哪些JS和初始展示是无关的,将那些没必要要的js延迟加载
  • 手工进行分离,如能够先显示出按钮,但状态是不可点,等JS加载完成后再改为可点的

传输

js压缩可使用YUI CompressorClosure Compiler

gwt中的js压缩还针对gzip进行了优化,进一步减少传输的体积,具体请阅读On Reducing the Size of Compressed Javascript

css

比起js放底部,css放页面顶部就比较容易作到

@import

使用@import在IE下会因为css加载延后而致使页面展示比使用link标签慢,不过目前几乎没有人使用@import,因此问题不大,具体细节请参考don’t use @import

selector的优化

浏览器在构建DOM树的过程当中会同时构建Render树,咱们能够简单的认为浏览器在遇到每个DOM节点时,都会遍历全部selector来判断这个节点会被哪些selector影响到

不过实际上浏览器通常是从右至左来判断selector是否命中的,对于ID、Class、Tag、Universal和Page的规则是经过 hashmap的方式来查找的,它们并不会遍历全部selector,因此selector越精确越好,google page-speed中的一篇文档Use efficient CSS selectors详细说明了如何优化selector的写法

另外一个比较好的方法是从架构层面进行优化,将页面不一样部分的模块和样式绑定,经过不一样组合的方式来生成页面,避免后续页面顶部的css只增不减,愈来愈复杂和混乱的问题,能够参考Facebook的静态文件管理

工具

如下整理一些性能优化相关的工具及方法

Browserscope

以前提到的http://www.browserscope.org收集了各类浏览器参数的对比,如最大连接数等信息,方便参考

Navigation Timing

Navigation Timing是还在草案中的获取页面性能数据api,能方便页面进行性能优化的分析

传统的页面分析方法是经过javascript的时间来计算,没法获取页面在网络及渲染上所花的时间,使用Navigation Timing就能很好地解决这个问题,具体它能取到哪些数据能够经过下图了解(来自w3c)

目前这个api较新,目前只在一些比较新的浏览器上有支持,如Chrome、IE9,但也占用必定的市场份额了,能够如今就用起来

boomerang

yahoo开源的一个页面性能检测工具,它的原理是经过监听页面的onbeforeunload事件,而后设置一个cookie,并在另外一个页面中 设置onload事件,若是cookie中有设置且和页面的refer保持一致,则经过这两个事件的事件来衡量当前页面的加载时间

另外就是经过静态图片来衡量带宽和网络延迟,具体能够看boomerang

检测工具

reference

相关文章
相关标签/搜索