页面加载
首先,浏览器发起直接对目标html的请求,然后分析其中用到的资源并下载,浏览器有自己的规则来判断什么样的资源可以被并行下载,什么样的不可以,浏览器对加载顺序有着特殊的喜好:
JS的出现会延迟后续CSS的下载,因为JS会改变页面元素,浏览器会延迟整个页面的渲染直到JS被下载解释并执行,所以必须让CSS的链接在JS前面以达到尽可能的并行。
与浏览器支持的并发连接数有关
在HTTP 1.1协议中要求浏览器访问同一host的连接数不得大于2,但事实上当前绝大多数浏览器都违背了这一要求,具体参见:并发连接数对浏览器加载速度的测试,实际的默认连接数的多少跟操作系统以及浏览器版本有关。但我们基本上可以认为,浏览器的并发连接数大于6(忽略过时的浏览器,比如IE6)。
浏览器在进行每一次请求资源的过程中,都需要进行DNS Lookup来将域名翻译成IP地址并且新建一个TCP连接(如果没有keepalive或者keepalive timeout了),因此连接越多由此带来的overhead越大。而且,一旦资源文件超过了浏览器支持的最大并发数量,那么必定有资源要被延迟下载。比如加载某网页需要下载13个资源文件(包含原始的html)、全都是CSS不会产生JS延迟、每次请求耗时100ms,那么浏览器第一次连接用于请求html,第二到第七次连接并发请求2-7号资源,第八到第十三次连接并发请求8-13号资源,可以看到,浏览器在并发连接的情况下也用了300ms。而如果将13个文件合并成7个文件的话,用200+ms就能完成(单个文件增大后传输会稍慢,不过少了DNS Lookup以及TCP连接的overhead,整体性能会有一个飞跃)。
综上所述,并行下载和降低连接overhead需要达到一个平衡状态才是一个好的方案,片面的追求较少的连接数或较高的并行性都是不可取的。这个平衡状态是因站点而已的,网站管理员需要根据各自网站的特点选用合适的技术来提升访问效率(当然服务器的性能也是相当重要的因素)。
常用的技术
CSS Sprites,用来将不经常改动的小图片整合成一张大图片,在CSS中利用background-position、width和height来控制显示的区域。优点是能显著的减少连接数以减少overhead而且可以被复用。缺点是制作起来比较费功夫,而且没有什么好办法解决repeat的背景图(因为大小未知)。
Data URL 和 DHTML,通过Base64编码将二进制文件(比如图片)捆绑到HTML/CSS中。优点是制作简便,也能减少连接数。缺点是BASE64在一定程度上会增大文件大小(即使用了GZip压缩);浏览器也要重新解码显示,会带来一定的性能问题;最重要的是,无法被缓存,每次请求HTML/CSS都会加载一遍。
CDN,全称Content Delivery Network,即内容分发网络。现在有一定规模以及并发访问量需求的站点(比如网易和新浪等)都将各自的页面资源(CSS/JS/图片等)分发在不同的host主机上,能让浏览器同时从多个host上下载资源而且也能根据负载和网络状况等因素将用户的请求递交到离用户最近的主机上,提升可靠性。成本巨大,不是一般人能玩得起的。当然有一些cdn站点提供诸如jquery之类的服务,在jQuery官方下载可以看到介绍,经我试验下来微软的ajax.aspnetcdn.com响应速度最快,优点有很多,速度和稳定性咱就不提了,更重要的是对浏览者来说他们可能已经请求过该脚本并放在缓存中了。
研究了各自的利弊之后得出我博客主题所使用的策略:主要使用CSS Sprites,用Data URL来解决背景repeat问题。当然,这也是因站点而已的,对于小站点(比如我的博客)之类的,可以把所有用到的图片整合到一张图片中;对于那些大站点,就应该把相近的功能整合到一张图中,这样就算有调整,客户端也不用下载整张大图,只需要更新修改的部分就可以。不管是CSS Sprites还是Data URL都是针对网站本身的样式来说,不适合把内容中的图片(比如新闻中的图片)捆绑进HTML/CSS/图片中。
实现动态加载JS的方式:
iframe
document.write
head.appendChild(script) 可跨域
xhr请求,然后eval
xhr注入
逐个分析:
1、document.write
示例:
<script language="javascript"> document.write("<script src='test.js'><\/script>"); </script>
2、head.appendChild(script)
var dynamicLoading = { css: function(path){ if(!path || path.length === 0){ throw new Error('argument "path" is required !'); } var head = document.getElementsByTagName('head')[0]; var link = document.createElement('link'); link.href = path; link.rel = 'stylesheet'; link.type = 'text/css'; head.appendChild(link); }, js: function(path){ if(!path || path.length === 0){ throw new Error('argument "path" is required !'); } var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.src = path; script.type = 'text/javascript'; head.appendChild(script); } }
调用方法:
//动态加载 CSS 文件 dynamicLoading.css("test.css"); //动态加载 JS 文件 dynamicLoading.js("test.js");
<script type="text/javascript"> var script = document.createElement("script"); if (script.readyState) { //IE
script.onreadystatechange = function () { if (script.readyState == "loaded" || script.readyState == "complete") { alert("i am callBack1"); } }; } else { //其他浏览器
script.onload = function () { alert("i am callBack"); }; }
script.src = "Scripts/a.js"; document.getElementsByTagName("head")[0].appendChild(script); </script>
方法 | 说明 |
---|---|
XHR Eval | 通过 Ajax 方式获取代码,并通过 eval 方式执行代码。 |
XHR Injection | 通过 Ajax 方式获取代码,并在页面上创建一个 script 元素,将 Ajax 取得的代码注入。 |
Script in Iframe | 通过 iframe 加载 js。 |
Script DOM Element | 使用 JavaScript 动态创建 script DOM 元素并设置其 src 属性。 |
Script Defer/Async | 严格来说,这一条不算是动态加载外部脚本的方法,但很多动态加载外部脚本的方法里都会用到 sctipt 的 defer 或 async 属性,所以也把它单独列在这儿。这个方法利用 script 的 defer 属性,让脚本“推迟”执行,不阻塞页面加载,或者设置 async 属性,让脚本异步执行。遗憾的是这两个属性不是所有浏览器都支持。 |
document.write Script Tag | 通过 document.write 把 HTML 标签 script 写入到页面中。 |
cache trick | 先使用自定义的 script 的 type 属性(如 <script type=”text/cache” …),甚至使用 Image、Object 等 HTML 对象将 js “预下载”(下载到浏览器缓存里),等真正需要执行对应代码时再将它真正地插入页面中。 |
Web Worker | 部分浏览器支持 web worker 功能,可以创建一个 worker 在后台工作,包括加载外部脚本。 |
各种方式各有优缺点,比如能否跨域、是否会阻塞其它资源的下载(能否并行下载)、能否管理控制执行顺序、耗费的资源、是否兼容各大浏览器等(部分方法的特性可参考这儿)。事实上,如果仅仅只是想把外部 js 动态加载到页面上的话还是很简单的,但如果可能要同时加载多个 js ,希望它们能尽可能快地下载(并行下载),并且有时候可能希望它们能保证执行顺序,而且要兼容各大主流浏览器,就会有一点复杂了。为了实现这样的目标,LABjs 使用了上面方法中的三种,分别是:Script DOM Element、cache trick、XHR Injection。下面我们先详细看一下这三种加载 js 方式的优缺点。
Script DOM Element
这是最常用的方式,它的优点很多:可以跨域、可以加载任何格式的外部 js(不需要对外部 js 进行重构)、不会阻塞其它资源的下载、实现简单。并且,在 Firefox/Opera 下,通过这种方式插入多个 js 脚本,浏览器会并行下载这些 js (同时下载几个取决于浏览器的并行连接数),同时还能保证它们的执行顺序与它们被插入页面的顺序相同。不过,在 IE(以及 Safari/Chrome)下,如果用这种方式同时插入多个 js,这些 js 也会并行下载,但浏览器不能保证这些 js 的执行顺序,哪个先下载完浏览器就会先执行哪个。
从上面可以看到,Script DOM Element这种方式在 Firefox/Opera 下的表现近乎完美,事实上,在这些浏览器下,Script DOM Element 也是 LABjs 默认使用的方式。
cache trick
上面我们看到,Script DOM Element方式可以满足 Firefox/Opera 下的需求了,那么 IE/Safari/Chrome 下怎么办呢?LABjs 中使用了一种 cache trick 的手段,它创建一个 script 标签,并将其 type 设为 “text/cache” ,把它插入页面。这种 trick 似乎是 LABjs 的作者 @getify 首先提出来的。这个 “text/cache” 只是为了语义上有意义,你完全可以把 type 属性设为任何不为 “text/javascript” 的值,比如 “love/oldj” 等。
根据 LABjs 作者博客上的文章,在 IE/Safari/Chrome 这三个浏览器下,如果一个 script 元素的 type 属性为一个类似 “text/cache” 这样的浏览器不认识的值,浏览器仍然会正常下载这些 js ,并且下载完成后正常触发 onload 事件,但是它们将不会执行这些脚本。于是,通过这样的方式可以先将 script 加载到浏览器缓存中,等对应的 js 需要被执行时,再创建一个新的 script 元素,设置其 type 为正确的值,src 为刚才“预下载”的脚本的值,将其插入页面,这样,这个 script 将会从缓存(而不是网络)瞬间加载对应的 js,并立即执行。通过这样的方式,LABjs 在 IE/Safari/Chrome 等浏览器下实现了脚本的预加载以及执行顺序管理。
另外,”text/cache” 这种 trick 在 Firefox/Opera 下是不能工作的,因为这两种浏览器会拒绝下载它们不认识的 type 的 script,这样也就无法“预加载”了。不过这不会造成问题,因为这两种浏览器可以直接通过上面的 Script DOM Element 的方式来加载外部脚本。(Firefox 4 曾试图更改相关特性,结果导致 getify 的强烈不满,具体事件可看这里。)同时,这种方法需要浏览器支持并且开启缓存,如果浏览器禁用或不支持缓存,也就无法“预加载”了,而且更糟糕的是,几乎没有 js 方法能检查用户浏览器是否支持并开启了缓存。
XHR Injection
cache trick 方式虽然可以实现并行下载、管理执行顺序,但毕竟是一种很依赖于特定浏览器特定版本的特性的方法,如果万一哪一天某个浏览器某个新版本改变了它的特性,这种方式可能就失效了,作者本人也觉得它很丑。所以,如果 LABjs 检测到要下载的外部 js 与当前页面是同域并且浏览器不为 Firefox/Opera(不能保证执行顺序与插入顺序一致)的话,它会优先采用 XHR Injection 的方式。
以上内容是各种摘抄、总结、还有自己的小实验。