规则5 - 将样式表放在顶部
将样式表放在文档底部就会导致在浏览器中阻止内容逐步呈现。为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容异步呈现。规则5对于加载页面所需的实际时间没有太多影响,它影响更多的是浏览器对这些组件顺序的反应。实际上,用户感觉缓慢的页面反而是可视化组件加载得更快的页面。在浏览器和用户等待位于底部的样式表时,浏览器会延迟显示任何可视化组件,这一现象,被称之为“白屏”。
为了避免白屏,请将样式表放在文档顶部的HEAD
中。经过这样修改后的网站,会解决白屏的情况,不管页面的是如何加载的————在新窗口打开、重新加载或者作为主页,页面都是逐步呈现的。
将样式表包含在文档中有两种方式,使用LINK
标签和@import
规则。
显然,相当于@import
语法,LINK
标签语法更简单,而且使用LINK
标签来代替@import
还能带来性能上的收益,@import
可能会导致白屏现象,即使把@import
规则放在HEAD
标签中的也是如此,使用@import
规则会导致组件下载时的无序性。
前面就说过,相比于将样式表放在底部,选择将样式放在顶部,响应时间会更长,但是会给用户更好的体验。因为它要首先下载样式表,尽管这不是呈现页面时所必需的,这就使后面组件的下载时间延长了,尽管花了更多时间来下载所需的组件,但用户感觉显示的更快,因为页面是逐步呈现的。所以还是要使用LINK
标签将样式表放在文档的HEAD
中
为什么浏览器以这种方式工作呢
样式表在页面中的位置并不影响下载时间,但是会影响页面的呈现。如果样式表仍在加载,构建呈现树就是一种浪费,因为在所有样式表加载并解析完毕之前无需绘制任何东西。否则,在其准备好之前显示内容会遇到FOUC
(无样式内容的闪烁 Flash of Unstyled Content)问题
样式表被放在底部,当页面逐步加载时,文字先显示,然后是图片。最后,在样式表正确地下载并解析之后,已经呈现的文字和图片要用新的样式重绘了,这就是无样式内容的闪烁。
白屏是浏览器在尝试修改前端工程师所犯的错误——将样式表放在文档比较靠后的位置。白屏是对FOUC
问题的弥补。浏览器可以延迟呈现,直到所有的样式表下载完之后,这就导致了白屏。反之,浏览器不呈现白屏来弥补这一问题,就可以逐步呈现,但是要承担闪烁的风险。
小结
当将样式表放在文档底部时,浏览器会出现FOUC
,因为页面逐步加载并解析,此时样式表还未下载完成,等到样式表下载完成时,页面就会产生重绘,就会出现FOUC
,为了处理FOUC
的问题,浏览器选择使用白屏的方式来呈现直到所有的样式表下载完成后,再绘制页面。当然,白屏和FOUC
给用户的体验都不是很好,所以需要将样式表放在文档的顶部,即使这延迟了其他组件的下载时间,但是可以给用户更好的体验,页面是逐步呈现的。所以规则5建议,使用LINK
标签将样式表放在文档的HEAD
中
规则6 - 将脚本放在底部
规则6建议将脚本放在底部,这样页面即可以逐步呈现,也可以提高下载的并行度。
如果脚本下载需要10秒钟,将脚本放在顶部,那么页面的下半部分(dom部分)要花大约10秒才能显示出来,因为脚本阻塞了并行下载。而且会影响逐步呈现,在使用脚本时,对于所有位于脚本以下的内容,逐步呈现都被阻塞了。将脚本放在页面越靠下的地方,意味着越多的内容能够逐步地呈现。
并行下载
为什么HTTP
请求的数量会影响响应时间呢?浏览器不能一次将他们都下载下来吗?
对此的解释要回到HTTP 1.1
规范,该规范建议浏览器从每个主机名并行地下载两个组件。很多web页面需要从一个主机名下载所有的组件,查看这些HTTP
请求会发现他们是呈阶梯状的。
如果一个web页面平均地将其组件分别放在两个主机名下,整体响应时间将可以减少大约一半,当然也可以将并行下载数增加到每个主机名超过两个(现浏览器厂商设置的并行下载数量大多是这3种值,2个、4个或6个)
前端工程师与其依赖用户来修改浏览器设置,不然简单地使用CNAME
(DNS
别名)来将组件分别放到多个主机名中,但增加并行下载数量并不是没有开销,其优劣取决于你的带宽和CPU
速度,过多的并行下载反而会降低性能。而雅虎相关研究表明,使用两个主机名比使用1、4、或10个主机名能带来更好的性能。
脚本阻塞下载
并行下载的优点是明显的,然而,在下载脚本时并行下载实际上是被禁用的————即使使用了不同的主机名,浏览也不会启动其他的下载,其中一个原因是,脚本可能使用document.write
来修改页面内容,因此浏览器就会等待,以确保页面能够恰当的布局。
在下载脚本时浏览器阻塞并行下载的另外一个原因是为了保证脚本能够按照正确的顺序执行,如果并行下载多个脚本,就无法保证响应是按照特定顺序到达浏览器的。例如,后面的脚本比页面中之前出现的脚本更小,就会更快下载完成并执行,如果它们之间存在依赖关系,不按照顺序执行就会导致JS
代码错误。
至此,脚本对web页面的影响就清楚了。
- 脚本会阻塞对其后面内容的呈现
- 脚本会阻塞对其后面组件的下载
如果将脚本放在页面顶部,正如通常的情况那样,页面中的所有东西都位于脚本之后,整个页面的呈现和下载都会被阻塞,直到脚本加载完毕。由于整个页面的呈现被阻塞,就会导致之前所说的白屏现象,逐步呈现对于用户体验是非常重要的,但缓慢的脚本下载延迟了用户所期待的反馈。此外,由于并行下载数的减少,不管图片显示的多快,都要被延迟。
所以放置脚本的最好地方就是页面的底部,这不会阻止页面内容的呈现,而且页面中的可视化组件可以尽早下载。
小结
脚本能够阻塞在其之后其他脚本的下载,因为脚本中的操作可能会导致页面变化,所以浏览器会等待脚本的下载完成,确保能够恰当的布局,而且如果之后的脚本依赖之前脚本,如果是并行下载,不去阻塞,很有可能之后的脚本会先行下载完成导致JS
代码错误。
同样的,脚本能够阻塞在其之后的其他组件的下载,如果将其放在页面的顶部,在其之后的可视化组件(dom树)就会被延迟下载,那么用户等待反馈的时间就变长。所以将脚本移到页面底部是规则6的建议。
规则7 - 避免css表达式
CSS
表达式是动态设置CSS
属性的一种强大且危险的方式;例如,可以使用CSS
表达式将背景色设置为每小时变化一次。
background-color: expression((new Date()).getHours()%2 ? "red" : "blue");
如上所示,expression
方法接受一个JS
表达式,CSS
属性将被设置为对JS
表达式进行求值的结果。
另外还有别的用处,例如,IE
不支持min-width
属性,CSS
表达式就是解决这一问题的方法,下面的示例确保一个页面的宽度至少是600
像素,IE
识别表达式,而其他浏览器识别静态设置的min-width
属性。
width: expression(document.body.clientWidth < 600 ? "600px" : "auto");
min-width: 600px;
表达式的问题在于对其进行求值的频率比人们期望的要高,它们不只在页面呈现和大小改变时求值,当页面滚动、甚至用户鼠标在页面上拖拽时都要求值。
当然,也有解决办法,如果CSS
表达式必须被求值一次,那么可以在只一次执行中重写它自身。
CSS
表达式调用了altBgcolor
函数,而这函数将样式的background-color
属性设置为了一个明确的值,并移除了CSS
表达式(行内样式权重最大)。之后用户无论是改变大小、滚动页面、拖拽鼠标,CSS
表达式都只会被执行一次。
还有另外一种处理方式,通过事件来处理,绝大多数使用CSS
表达式的情形,都可以找到不需要CSS
表达式的替代方法。CSS
表达式从自动绑定到浏览器事件中获益,但这也是它的缺陷。除了使用CSS
表达式之外,前端工程师还可以尝试使用事件处理器来为特定的事件提供所期望的动态行为。这就避免了在无关事件发生时对表达式的求值。例如之前的min-width
示例,就可以改写为:
function setMinWidth () {
var aElements = document.getElementsByTagName('p');
for (var i = 0, len = aElements.length; i < len ; i++) {
aElements[i].style.width = document.body.clientWidth < 600 ? "600px" : "auto";
}
window.onresize = setMinWidth;
}
小结
避免使用CSS
表达式,因为它们不只在页面呈现和大小改变时求值,当页面滚动、甚至用户鼠标在页面上拖拽时都要求值,产生过多的开销。如果一定要使用,可以使用一次性表达式或者使用事件处理器来代替。
规则8 - 使用外部JS和CSS
内联相比外置来说,内联更快一些,尽管所需下载的总数据量是相同的,内联JS和CSS还是比外置JS
和CSS快30%-50%,因为外置需要承担多个HTTP
请求带来的开销,尽管外置可以从样式表和脚本的并行下载中获益,但一个HTTP
请求和多个HTTP
请求之间的差距导致内联更快一些,尽管结果如此,现实中还是使用外部文件会产生较快的页面,因为外置文件有机会被浏览器缓存起来,当遇到这种情况,因为HTML
文档不会被缓存(因为是动态的,用户可操作,经常会改变),每次请求HTML
文档都会重新下载内联的JS
和CSS
,相反,如果是外置,浏览器就能缓存它们,HTML
文档的大小减小,而且不会增加HTTP
请求的数量。
以下3个因素用来判断是选择内联还是外置
页面浏览量
每个用户的产生的页面浏览量越少,内联JS
和CSS
的优势越大,因为如果用户每个月只访问你的网站一次,在每次访问时,外置的JS
和CSS
很有可能从浏览器的缓存中移除,即使该组件拥有一个长久的Expires
头部(因为可能会被其他的网站的缓存给”挤走“),相反,如果用户能够产生很多的页面浏览,浏览器很可能将具有长久Expires
头的外部文件放在其缓存中。使用外部JS
和CSS
带来的收益会随着用户每月的页面浏览量或用户每会话差生的页面浏览量的增长而增加。
空缓存 VS 完整缓存
如果你的网站本质上能够为用户带来高完整缓存率,使用外部文件的收益就更大。如果不大可能产生完整缓存,则内联是更好的选择。
组件重用
如果你的网站中每个页面都使用了相同的JS
和CSS
,使用外部文件可以提高这些组件的重用率。在这种情况下,使用外部文件更加具有优势,因为在用户在页面件导航时,JS
和CSS
组件已经位于浏览器的缓存中了。
相反,如果重用率非常低,使用内联优势更大,但是通常情况下,页面之间的JS
和CSS
的重用既不可能100%重叠,也不可能100%无关,在这种中间情形下,一个极端就是为每个页面提供一组分离的外部文件,这种方式的缺点在于,每个页面都强制用户产生一组令响应时间变慢的HTTP
请求,但是这种方式对于普通用户只访问一个页面和很少进行跨页访问的网站来说是有意义的。
另一个极端是创建一个单独的、联合了所有JS
的文件和一个单独的、联合了所有CSS
的文件,这样就只要求用户生成一个HTTP
请求,但是这增加了用户首次进行页面浏览时的下载数据量,在这种情况下,用户浏览页面是下载的JS
和CSS
要多余所需的数量,而且,在任何一块独立的脚本或样式改变后,都需要更新这个文件,使所有用户已经缓存了的当前版本无效,这种情况对于用户每月会话数量较高、用户在一个会话中访问多个不同页面的网站来说是有意义的(因为这种方式了提高了用户后续浏览的响应速度)。
如果你可以找到一个平衡点,实现较高的重用度,则将JS
和CSS
部署到外部的优势更大一些,如果很低,还是内联更有意义一些。
主页
还有一点,就是主页(浏览器默认页的URL
)更适合使用内联,只要打开浏览器首先就会访问主页,然而,通常每个会话只产生一个主页的页面浏览量,而且每次都是空缓存并且重用率很低。
两全其美
即使你的网站所有的因素都偏向于内联,将所有JS
和CSS
都添加到页面中还是会感觉很低效,而且无法利用浏览器的缓存,这里介绍两项技术使你既可以获得内联的优势,同时也能缓存外部文件。
1、加载后下载
对于作为多次页面浏览量中第一次的主页,我们希望为主页内联JS
和CSS
,但又能为所有后续页面提供外部文件,这可以通过在主页加载完成后动态下载外部组件来实现,通过onload
事件,这就能够将外部文件放到浏览器的缓存中以便用户接下来访问其他页面。
2、动态内联
动态内联需要后端的支持,在用户第一次访问页面的时候,服务器发现没有cookie
,于是生成一个内联了组件的页面。然后服务器添加JS来在页面加载后动态下载外部文件,并设置cookie
,下次访问页面时,服务器看到了cookie
,就会生成一个使用外部文件的页面。
这种方式的美好之处在于它的宽容,即便cookie
的状态和缓存的状态不符,页面也能照常工作,只是没有本应该的那么优化而已。
如果用户重新打开浏览器,基于会话的cookie
会消失,但是组件依然在缓存中,这样缓存就没有被利用到,但是此时服务器会生成一个内联组件的页面,页面照常访问,当然也可以将cookie
从基于会话改为短期的(数小时或数天来)来解决这个问题。
也有可能,这些组件并不在浏览器缓存中,但是cookie
存在,这样服务器会生成一个外联组件的页面,重新去下载组件,页面也可以照常访问。重要的是,从全体用户的角度来看,它通常能够在内联和外部文件中做出智能的选择,从而改善响应时间。
小结
1、内联比外置更快,因为HTTP
请求更少,但是即使这样,外置文件可以被浏览器缓存的优势更大。
2、通过页面浏览量、缓存程度、组件重用程度三个因素来选择是内联还是外置,如果页面访问量少使用内联的优势越大,因为缓存可能会被清理掉。在组件重用方面,如果重用度很高使用外部文件就更有优势,因为会被缓存,之后的页面重用不会产生额外的HTTP
请求。
3、大多数网站重用度的处于中间的情况,这样就会有两个极端的处理方式,每个页面都使用单独的一组JS
和CSS
,或者打包所有的JS
和CSS
生成相应的一个JS文件和CSS文件,这两种方式各有优劣,是用来处理重用度中间情况时项目的文件结构该如何抉择的选项。
4、重要的是,不管内联的优势多大,依然推荐使用外置文件,因为将所有的JS
和CSS
都内置在页面中还是感觉很低效的,而且无法被缓存。庆幸的是,有两全其美的解决方案,加载后下载和动态内联,前者能够在访问主页后,将外部文件放到浏览器的缓存中,以便用户接下来访问其他页面。后者宽容度较高,不会出错,页面总是能正常呈现,而且通常情况下可以智能的选择内置还是外联,优势在于可以实现用户没访问过的页面不会下载该页面的JS
和CSS
文件,减少下载量;而当用户访问过之后,就会返回使用了外置文件的页面,利用外置文件可以被缓存的优势。