使用“动态内联”的方式加载javascript、CSS

“动态内联”这个词是出自“高性能网站建设指南”中第八章“规则8—使用外部Javascript和CSS”,它的原理是基于权衡页面中内联Javascript、CSS和外联Javascript、CSS时的比较。“纯粹而言,内联快一些”,因为外联的时候需要向服务器发送HTTP请求,这需要更多的加载时间,而相对于内联的方式,页面的总大小(包括HTML内容、js、css等等)的一定的,直接下载的方式当然会更快。但是,从“可缓存”、“组件重用”等方面来考虑,外联的方式更占有优势,可以更快的加载页面。

所以,这里就存在一个权衡点:到底是内联,还是外联?但是,有没有可能,让内联的优势和外联的优势结合起来呢?

第一种方案就是书中提出的“加载后加载”,意思就是当可以预测到用户将会接下来访问什么页面的时候,可以通过当前页面的onload事件,来加载后续的组件,这样当用户在访问接下来的页面的时候,可以从缓存中获取组件,使得加载速度提升。这种方式是比较普遍的了。它的主要应用在于首页中,比如(Google,百度,Yahoo等等)。

第二种方案就是标题说的“动态内联”。如果服务器知道一个组件是否在浏览器的缓存中,它就可以在内联或者使用外联外部文件之间作出最佳的选择。服务器当然不可能直接查看浏览器的缓存,不过可以通过cookie的方式来检测是否缓存了组件。如果指定的cookie不存在,说明浏览器没有缓存相应的组件,使用内联的方式加载组件;当cookie存在,则使用外联的方式加载组件,从缓存中读取组件。

理论说起来容易,实践起来还是需要技巧的,而基本原理就是:使用Javascript设置cookie,服务器端的语言(php,jsp,asp.net等等)来检测cookie并读取文件,例如PHP中使用file_get_contents来读取文件。下面提供一个简单的例子(测试页面):

<?php
//检查cookie是否存在,存在则采用外联的方式,否则采用内联的方式
if($_COOKIE["CSS"]){
  echo '<link rel="stylesheet" href="test.css" />';
}else{
  echo "<style>".file_get_contents("test.css")."</style>";
}
?>
<script>
"use strict"; //Don't cancel
//首先检测是否设置了cookie,如果没有则设置,并通过iframe的方式先缓存组件
//在test.html中加载了test.css文件,先在浏览器缓存起来。
if(!(/CSS/.test(document.cookie))){
  document.cookie="CSS=1";
  var iframe=document.createElement("iframe");
  iframe.src="test.html";
  iframe.width="0";
  iframe.height="0";
  document.body.appendChild(iframe);
}
</script>

如上代码所示,在页面第一次访问的时候,此时cookie还没有设置,采用内联的方式来加载组件,当刷新页面或者浏览其他页面的时候,会检测cookie并从缓存中读取test.css组件。这样就结合使用了内联和外联的优势,对页面加载速度起到了一定的优化效果。对于“动态内联”更多的描述,书中提到:

这种方式的美好之处在于它的宽容,即便cookie的状态和缓存不匹配,页面也能够工作,只是没有本应该的那么优化而已。基于会话的cookie技术在内联时会发生错误,即便组件已经被放到浏览器缓存中了——如果用户重新打开浏览器,基于会话的cookie会消失,但组件依然存在于缓存中。将cookie从基于会话的改为短期的(数小时或数天)可以解决这个问题,但当它们并不在浏览器缓存中时,使用外部文件又会出错。不管出现哪种情况,页面都能够正常工作。

对于上面的描述,我存在一个问题:当cookie存在,而缓存不存在的时候,就over了。除非在每一个页面的js中都加载一个iframe来缓存组件,比如修改上面的js代码如下:

  document.cookie="CSS=1";
  var iframe=document.createElement("iframe");
  iframe.src="test.html";
  iframe.width="0";
  iframe.height="0";
  document.body.appendChild(iframe);

上面的方式就是不检测cookie是否存在,而是每个页面每次浏览的时候都动态生成一个iframe来缓存文件。这确实没有必要的。

主页面和iframe中加载资源的测试

对于iframe,我是比较少做测试的,今晚由于我的一个问题“有没有办法让页面HTML内容和其他资源并行加载”引发了我和瓶子之间的一番关于iframe的测试。

首先是由于我的一个误解“iframe会阻塞后面资源的下载”以及上面的问题引发的“浏览器解析页面和下载资源的顺序问题”开始。在Webkit的官方博客中发表了这么篇文章:《How WebKit Loads a Web Page》,讲述页面解析HTML和下载资源的机制。文中就说明了Webkit在加载一个页面的时候存在两个下载通道,一个用于加载html document,另一个用于加载子资源(图片、CSS、JS等等),并且这两个通道存在时间顺序问题(查看原文或者下面的图示即可了解),这就说明了“让页面HTML内容和其他资源并行加载”就目前来说是不可能实现的,因为这是由浏览器内部的加载页面的机制决定了。

在这篇文章里,跟瓶子就“Frames”这个字眼见解有些不同,他认为这就是跟普通页面的iframe一样的意思(这也是接下来一堆测试iframe的导火线),而我曾经在某篇En文里看过Firefox中解析HTML页面的时候,也提到了Frame这个字眼,而它所说的Frame并不是普通html中的iframe,而是相当于一个“暗箱”,或者说是一个“容器”,来解析HTML页面内容。这个问题未得到证实,就不多说了。

之后我就凭记忆说了一句“加载iframe会阻碍后面的元素的加载”,我的本意是说,当iframe中有js、图片、css等资源需要加载时,会阻塞主页面中iframe后面资源的加载。可是经过他的测试,发现说“主页面的html和iframe的html是并行加载的”。刚开始我就纳闷了,就拿出《高性能网站建设进阶指南》来查看关于iframe的内容,发现书中并没有关于“加载iframe会阻碍后面的元素的加载”的论述,而是叙述了当iframe前面有CSS或者JS资源的时候,会阻塞iframe的加载(测试页面),如果放到iframe其后的话,则会iframe和主页面会并行加载(测试页面)。同时书中也指明了一点“iframe会阻碍主页面onload事件的触发”。

经过这一番测试,不得不承认是我的失误,记错了~

但是我在测试过程中,通过观察firebug和HTTPWatch生成的HTTP瀑布图发现了一个比较有趣的事情:当iframe在js、图片、css等主页面资源的前面的时候,在HTTP瀑布图中显示iframe内的资源都是后于主页面的资源加载的。我就纳闷了,这么奇怪的问题。接下来我就写了一个测试页面,页面中iframe内的js文件中定义了一个函数,而且文件有100多K大小;在主页面的fulljsmin.js文件中调用该函数,文件很小(测试页面)。通过测试更有意思的是,成功调用了该函数,没有出现未定义的错误,这不是跟HTTP瀑布图中显示的不符合吗?下面是在firebug和HTTPWatch下的HTTP瀑布图:


同时,在Chrome下用Inspect插件来测试,也都发现了这样的情况。虽然在IE8下HTTPWatch中显示iframe并没有和主页面并行加载,但是出现的问题都基本跟上面所述一致。这难道是firebug、HTTPWatch、Inspect的一个bug?不可能同时都出现这个问题吧?我想问题还应该是出在页面加载iframe的机制上。

对于这个问题,我最初的想法是主页面在加载iframe的时候会开启另外一个进程来加载iframe里面的资源,但是浏览器普遍都只能是两个资源并行下载,iframe内也同样难逃这条规则。所以这个想法就错误了,而瓶子对于这个页面同时加载主页面的资源和iframe内的资源的看法是:

  1. 父页面html加载->html解析
  2. 如果有iframe,就开一个iframe线程下载html,重复步骤1
  3. 把页面其他要请求的元素放入一个加载队列,这个队列是全局的,如果是iframe的html解析的时候,也会被加入到这个队列里。但是最终会出现的顺序可能有所不同,有的浏览器应该会等html全部分析完再开始执行加载队列,有的则是一边解析一边对队列进行执行。对于第一种情况,出现的顺序应该是:主页面的内容先加载,iframe里面的内容会最后连续加载对于第二种情况,出现的顺序可能是交叉的,不过iframe的加载应该大多集中在一处。

第一点看法是毋庸置疑的,但是第二点所说的“开一个iframe线程下载html,重复步骤1”,这点或许需要说明一下,这开一个iframe线程是受到浏览器通常只能同时两个并发下载数的限制的,所以这里的新开线程可能会受到CSS、JS文件的影响,就像开头说的,CSS、JS在iframe前面的时候,会阻塞iframe的加载(测试页面,这次在页面中嵌套了两个iframe)。从下图可以说明,在iframe前面有CSS、js等外联文件的时候,阻塞是比较严重的:

对于第三点的将iframe中的资源加入加载队列,这里我就有点迷糊于“在什么位置加入加载队列”。如果简单的从HTTP瀑布图来说的话,第一种情况是比较在理的:在页面开始加载资源的时候,自动生成一个加载队列,遇到iframe的时候,就将iframe中的资源加载到队列的末尾,可是这又不符合上面的函数调用的测试。所以我初步总结为:在Firefox、Chrome、IE中,当主页面开始加载子资源的时候,自动生成一个加载队列,当遇到iframe的时候,就把iframe里的子资源加入到加载队列里,主页面后续的子资源再加入到加载队列中。当页面内有几个iframe的时候,也是这个理。

写本文时我都还存在很多的疑问需要慢慢揭开,同时也希望对这方面有研究的朋友,能指明一二,共同探讨~