几乎每一个前端程序员都知道应该把script标签放在页面底部。关于这个经典的论述可以追溯到nicholas的?high performance javasript?这本书的第一章loading and execution中,他之所以建议这么做是因为:
put all <script> tags at the bottom of the page, just inside of the closing </body> tag. this ensures that the page can be almost completely rendered before script execution begins.
简而言之,如果浏览器加载并执行脚本,会引起页面的渲染被暂停,甚至还会阻塞其他资源(比如图片)的加载。为了更快的给用户呈现网页内容,更好的用户体验,应该把脚本放在页面底部,使之最后加载。
为什么要在标题中使用“再”这个字?因为在工作中逐渐发现,我们经常谈论的一些页面优化技巧,比如上面所说的总是把脚本放在页面的底部,压缩合并样式或者脚本文件等,时至今日已不再是最佳的解决方案,甚至事与愿违,转化为性能的毒药。这篇文章所要聊的,便是展示某些不被人关注的浏览器特性或者技巧,来继续完成资源加载性能优化的任务。
一. preloader什么是preloader首先让我们看一看这样一类资源分布的页面:
<head>
<link rel=stylesheet type=text/css href=>
<script type=text/javascript></script>
</head>
<body>
<img src=>
<img src=>
<img src=>
<img src=>
<img src=>
<img src=>
<img src=>
<img src=>
<script type=text/javascript></script>
<script type=text/javascript></script>
<script type=text/javascript></script>
</body>
这类页面的特点是,一个外链脚本置于页面头部,三个外链脚本置于页面的底部,并且是故意跟随在一系列img之后,在chrome中页面加载的网络请求瀑布图如下:
值得注意的是,虽然脚本放置在图片之后,但加载仍先于图片。为什么会出现这样的情况?为什么故意置后资源能够提前得到加载?
虽然浏览器引擎的实现不同,但原理都十分的近似。不同浏览器的制造厂商们(vendor)非常清楚浏览器的瓶颈在哪(比如network, javascript evaluate, reflow, repaint)。针对这些问题,浏览器也在不断的进化,所以我们才能看到更快的脚本引擎,调用gpu的渲染等一推陈出新的优化技术和方案。
同样在资源加载上,早在ie8开始,一种叫做lookahead pre-parser(在chrome中称为preloader)的机制就已经开始在不同浏览器中兴起。ie8相对于之前ie版本的提升除了将每台host最高并行下载的资源数从2提升至6,并且能够允许并行下载脚本文件之外,最后就是这个lookahead pre-parser机制
但我还是没有详述这是一个什么样的机制,不着急,首先看看与ie7的对比:
以上面的页面为例,我们看看ie7下的瀑布图:
底部的脚本并没有提前被加载,并且因为由于单个域名最高并行下载数2的限制,资源总是两个两个很整齐的错开并行下载。
但在ie8下,很明显底部脚本又被提前:
并没有统一的标准规定这套机制应具备何种功能已经如何实现。但你可以大致这么理解:浏览器通常会准备两个页面解析器parser,一个(main parser)用于正常的页面解析,而另一个(preloader)则试图去文档中搜寻更多需要加载的资源,但这里的资源通常仅限于外链的js、stylesheet、image;不包括audio、video等。并且动态插入页面的资源无效。
但细节方面却值得注意:
比如关于preloader的触发时机,并非与解析页面同时开始,而通常是在加载某个head中的外链脚本阻塞了main parser的情况下才启动;也不是所有浏览器的preloader会把图片列为预加载的资源,可能它认为图片加载过于耗费带宽而不把它列为预加载资源之列;preloader也并非最优,在某些浏览器中它会阻塞body的解析。因为有的浏览器将页面文档拆分为head和body两部分进行解析,在head没有解析完之前,body不会被解析。一旦在解析head的过程中触发了preloader,这无疑会导致head的解析时间过长。preloader在响应式设计中的问题preloader的诞生本是出于一番好意,但好心也有可能办坏事。
filamentgroup有一种著名的响应式设计的图片解决方案responsive design images:
<html>
<head>
<title></title>
<script type=text/javascript src=./responsive-images.js></script>
</head>
<body>
<img src=./running.jpg?medium=_imgs/running.medium.jpg&large=_imgs/running.large.jpg>
</body>
</html>
它的工作原理是,当responsive-images.js加载完成时,它会检测当前显示器的尺寸,并且设置一个cookie来标记当前尺寸。同时你需要在服务器端准备一个.htaccess文件,接下来当你请求图片时,.htaccess中的配置会检测随图片请求异同发送的cookie是被设置成medium还是large,这样也就保证根据显示器的尺寸来加载对于的图片大小。
很明显这个方案成功的前提是,js执行先于发出图片请求。但在chrome下打开,你会发现执行顺序是这样:
responsive-images.js和图片几乎是同一时间发出的请求。结果是第一次打开页面给出的是默认小图,如果你再次刷新页面,因为cookie才设置成功,服务器返回的是大图。
严格意义上来说在某些浏览器中这不一定是preloader引起的问题,但preloader引起的问题类似:插入脚本的顺序和位置或许是开发者有意而为之的,但preloader的这种“聪明”却可能违背开发者的意图,造成偏差。
如果你觉得上一个例子还不够说明问题的话,最后请考虑使用picture(或者@srcset)元素的情况:
<picture>
<source src=med.jpg media=(min-width: 40em) />
<source src=sm.jpg/>
<img src=fallback.jpg alt= />
</picture>
在preloader搜寻到该元素并且试图去下载该资源时,它应该怎么办?一个正常的paser应该是在解析该元素时根据当时页面的渲染布局去下载,而当时这类工作不一定已经完成,preloader只是提前找到了该元素。退一步来说,即使不考虑页面渲染的情况,假设preloader在这种情形下会触发一种默认加载策略,那应该是”mobile first”还是”desktop first”?默认应该加载高清还是低清照片?
二. js loader理想是丰满的,现实是骨感的。出于种种的原因,我们几乎从不直接在页面上插入js脚本,而是使用第三方的加载器,比如seajs或者requirejs。关于使用加载器和模块化开发的优势在这里不再赘述。但我想回到原点,讨论应该如何利用加载器,就从seajs与requirejs的不同聊起。
在开始之前我已经假设你对requirejs与seajs语法已经基本熟悉了,如果还没有,请移步这里:
cmd标准:https://github.com/cmdjs/specification/blob/master/draft/module.mdamd标准:https://github.com/amdjs/amdjs-api/blob/master/amd.mdbtw: 如果你还是习惯在部署上线前把所有js文件合并打包成一个文件,那么seajs和requirejs其实对你来说并无区别。
seajs与requirejs在模块的加载方面是没有差异的,无论是requirejs在定义模块时定义的依赖模块,还是seajs