让我们再聊聊浏览器资源加载优化

阅读数:18086 2014 年 6 月 27 日

几乎每一个前端程序员都知道应该把 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 等。并且动态插入页面的资源无效。

但细节方面却值得注意:

  1. 比如关于 preloader 的触发时机,并非与解析页面同时开始,而通常是在加载某个 head 中的外链脚本阻塞了 main parser 的情况下才启动;

  2. 也不是所有浏览器的 preloader 会把图片列为预加载的资源,可能它认为图片加载过于耗费带宽而不把它列为预加载资源之列;

  3. 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 语法已经基本熟悉了,如果还没有,请移步这里:

BTW: 如果你还是习惯在部署上线前把所有 js 文件合并打包成一个文件,那么 seajs 和 requirejs 其实对你来说并无区别。

seajs 与 requirejs 在模块的加载方面是没有差异的,无论是 requirejs 在定义模块时定义的依赖模块,还是 seajs 在 factory 函数中 require 的依赖模块,在会在加载当前模块时被载入,异步,并且顺序不可控。差异在于 factory 函数执行的时机。

执行差异

为了增强对比,我们在定义依赖模块的时候,故意让它们的 factory 函数要执行相当长的时间,比如 1 秒:

// dep_A.js 定义如下,dep_B、dep_C 定义同理 

define(function(require, exports, module) {

    (function(second) {
        var start = +new Date();
        while (start + second * 1000 > +new Date()) {}
    })(window.EXE_TIME);

    // window.EXE_TIME = 1;此处会连续执行 1s

    exports.foo = function() {
        console.log("A");
    }
})

为了增强对比,设置了三组进行对照试验,分别是:

//require.js:
require(["dep_A", "dep_B", "dep_C"], function(A, B, C) {

});

//sea.js:
define(function(require, exports, module) {

    var mod_A = require("dep_A");
    var mod_B = require("dep_B");
    var mod_C = require("dep_C");
});

//sea.js(定义依赖但并不 require):
define(["dep_A", "dep_B", "dep_C"], function(require, exports, module){

}

接下来我们看看代码执行的瀑布图:

1.require.js:在加载完依赖模块之后立即执行了该模块的 factory 函数

2.sea.js: 下面两张图应该放在一起比较。两处代码都同时加载了依赖模块,但因为没有 require 的关系,第三张图中没有像第二张图那样执行耗时的 factory 函数。可见 seajs 执行的原则正如 CMD 标准中所述 Execution must be lazy。

我想进一步表达的是,无论 requirejs 和 seajs,通常来说大部分的逻辑代码都会放在模块的 factory 函数中,所以 factory 函数执行的代价是非常大的。但上图也同样告诉我们模块的 define,甚至模块文件的 Evaluate 代价非常小,与 factory 函数无关。所以我们是不是应该尽可能的避免执行 factory 函数,或者等到我们需要的指定功能的时候才执行对应的 factory 函数?比如:

document.body.onclick = function () {
    require(some_kind_of_module);
}

这是非常实际的问题,比如爱奇艺一个视频播放的页面,我们有没有必要在第一屏加载页面的时候就加载登陆注册,或者评论,或者分享功能呢?因为有非常大的可能用户只是来这里看这个视频,直至看完视频它都不会用到登陆注册功能,也不会去分享这个视频等。加载这些功能不仅仅对浏览器是一个负担,还有可能调用后台的接口,这样的性能消耗是非常可观的。

我们可以把这样称之为"懒执行"。虽然 seajs 并非有意实现如上所说的“懒执行”(它只是在尽可能遵循 CommonJS 标准靠近)。但“懒执行”确实能够有助于提升一部分性能。

但也有人会对此产生顾虑。

记得玉伯转过的一个帖子:SeaJS 与 RequireJS 最大的区别。我们看看其中反对这么做的人的观点

我个人感觉 requirejs 更科学,所有依赖的模块要先执行好。如果 A 模块依赖 B。当执行 A 中的某个操 doSomething() 后,再去依赖执行 B 模块 require('B'); 如果 B 模块出错了,doSomething 的操作如何回滚? 很多语言中的 import, include, useing 都是先将导入的类或者模块执行好。如果被导入的模块都有问题,有错误,执行当前模块有何意义?

而依赖 dependencies 是工厂的原材料,在工厂进行生产的时候,是先把原材料一次性都在它自己的工厂里加工好,还是把原材料的工厂搬到当前的 factory 来什么时候需要,什么时候加工,哪个整体时间效率更高?

首先回答第一个问题。

第一个问题的题设并不完全正确,“依赖”和“执行”的概念比较模糊。编程语言执行通常分为两个阶段,编译 (compilation) 和运行 (runtime)。对于静态语言 (比如 C/C++) 来说,在编译时如果出现错误,那可能之前的编译都视为无效,的确会出现描述中需要回滚或者重新编译的问题。但对于动态语言或者脚本语言,大部分执行都处在运行时阶段或者解释器中:假设我使用 Nodejs 或者 Python 写了一段服务器运行脚本,在持续运行了一段时间之后因为某项需求要加载某个 (依赖) 模块,同时也因为这个模块导致服务端挂了——我认为这时并不存在回滚的问题。在加载依赖模块之前当前的模块的大部分功能已经成功运行了。

再回答第二个问题。

对于“工厂”和“原材料”的比喻不够恰当。难道依赖模块没有加载完毕当前模块就无法工作吗?requirejs 的确是这样的,从上面的截图可以看出,依赖模块总是先于当前模块加载和执行完毕。但我们考虑一下基于 CommonJS 标准的 Nodejs 的语法,使用 require 函数加载依赖模块可以在页面的任何位置,可以只是在需要的时候。也就是说当前模块不必在依赖模块加载完毕后才执行。

你可能会问,为什么要拿 AMD 标准与 CommonJS 标准比较,而不是 CMD 标准?

玉伯在CommonJS 是什么这篇文章中已经告诉了我们 CMD 某种程度上遵循的就是 CommonJS 标准:

从上面可以看出,Sea.js 的初衷是为了让 CommonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上这个梦无法完全达成,也没有必要去达成。

更好的一种方式是,Sea.js 专注于 Web 浏览器端,CommonJS 则专注于服务器端,但两者有共通的部分。对于需要在两端都可以跑的模块,可以 有便捷的方案来快速迁移。

其实 AMD 标准的推出同时也是遵循 CommonJS,在 requirejs 官方文档的COMMONJS NOTES中说道:

CommonJS defines a module format. Unfortunately, it was defined without giving browsers equal footing to other JavaScript environments. Because of that, there are CommonJS spec proposals for Transport formats and an asynchronous require.

RequireJS tries to keep with the spirit of CommonJS, with using string names to refer to dependencies, and to avoid modules defining global objects, but still allow coding a module format that works well natively in the browser.

CommonJS 当然是一个理想的标准,但至少现阶段对浏览器来说还不够友好,所以才会出现 AMD 与 CMD,其实他们都是在做同一件事,就是致力于前端代码更友好的模块化。所以个人认为依赖模块的加载和执行在不同标准下实现不同,可以理解为在用不同的方式在完成同一个目标, 并不是一件太值得过于纠结的事。

懒加载

其实我们可以走的更远,对于非必须模块不仅仅可以延迟它的执行,甚至可以延迟它的加载。

但问题是我们如何决定一个模块是必须还是非必须呢,最恰当莫过取决于用户使用这个模块的概率有多少。Faceboook 早在 09 年的时候就已经注意到这个问题:Frontend Performance Engineering in Facebook : Velocity 2009,只不过他们是以样式碎片来引出这个问题。

假设我们需要在页面上加入 A、B、C 三个功能,意味着我们需要引入 A、B、C 对应的 html 片段和样式碎片 (暂不考虑 js),并且最终把三个功能样式碎片在上线前压缩到同一个文件中。但可能过了相当长时间,我们移除了 A 功能,但这个时候大概不会有人记得也把关于 A 功能的样式从上线样式中移除。久而久之冗余的代码会变得越来越多。Facebook 引入了一套静态资源管理方案 (Static Resource Management) 来解决这个问题:

具体来说是将样式的“声明”(Declaration) 和请求 (Delivery) 请求,并且是否请求一个样式由是否拥有该功能的 html 片段决定。

当然同时也考虑也会适当的合并样式片段,但这完全是基于使用算法对用户使用模块情况进行分析,挑选出使用频率比较高的模块进行拼合。

这一套系统不仅仅是对样式碎片,对 js,对图片 sprites 的拼合同样有效。

你会不会觉得我上面说的懒加载还是离自己太远了? 但然不是,你去看看现在的人人网个人主页看看

如果你在点击图中标注的“与我相关”、“相册”、“分享”按钮并观察 Chrome 的 Timeline 工具,那么都是在点击之后才加载对应的模块

三. Delay Execution

利用浏览器缓存

脚本最致命的不是加载,而是执行。因为何时加载毕竟是可控的,甚至可以是异步的,比如通过调整外链的位置,动态的创建脚本。但一旦脚本加载完成,它就会被立即执行 (Evaluate Script),页面的渲染也就随之停止,甚至导致在低端浏览器上假死。

更加充分的理由是,大部分的页面不是 Single Page Application,不需要依靠脚本来初始化页面。服务器返回的页面是立即可用的,可以想象我们初始化脚本的时间都花在用户事件的绑定,页面信息的丰满 (用户信息,个性推荐)。Steve Souders发现在 Alexa 上排名前十的美国网站上的 js 代码,只有 29% 在 window.onload 事件之前被调用,其他的 71% 的代码与页面的渲染无关。

Steve Souders 的ControlJS是我认为一直被忽视的一个加载器,它与 Labjs 一样能够控制的脚本的异步加载,甚至 (包括行内脚本,但不完美) 延迟执行。它延迟执行脚本的思路非常简单:既然只要在页面上插入脚本就会导致脚本的执行,那么在需要执行的时候才把脚本插入进页面。但这样一来脚本的加载也被延迟了?不,我们会通过其他元素来提前加载脚本,比如 img 或者是 object 标签,或者是非法的 mine type 的 script 标签。这样当真正的脚本被插入页面时,只会从缓存中读取。而不会发出新的请求。

Stoyan Stefanov在它的文章Preload CSS/JavaScript without execution中详细描述了这个技巧, 如果判断浏览器是 IE 就是用 image 标签,如果是其他浏览器,则使用 object 元素:

window.onload = function () {

    var i = 0,
        max = 0,
        o = null,

        preload = [
            // list of stuff to preload    
        ],

        isIE = navigator.appName.indexOf('Microsoft') === 0;

    for (i = 0, max = preload.length; i < max; i += 1) {

        if (isIE) {
            new Image().src = preload[i];
            continue;
        }
        o = document.createElement('object');
        o.data = preload[i];

        // IE stuff, otherwise 0x0 is OK
        //o.width = 1;
        //o.height = 1;
        //o.style.visibility = "hidden";
        //o.type = "text/plain"; // IE 
        o.width  = 0;
        o.height = 0;


        // only FF appends to the head
        // all others require body
        document.body.appendChild(o);
    }

};

同时它还列举了其他的一些尝试,但并非对所有的浏览器都有效,比如:

  • 使用 <link> 元素加载 script,这么做在 Chrome 中的风险是,在当前页有效,但是在以后打开需要使用该脚本的页面会无视该文件为缓存

  • 改变 script 标签外链的 type 值,比如改为 text/cache 来阻止脚本的执行。这么做会导致在某些浏览器 (比如 FF3.6) 中压根连请求都不会发出

type=prefetch

延迟执行并非仅仅作为当前页面的优化方案,还可以为用户可能打开的页面提前缓存资源,如果你对这两种类型的 link 元素熟悉的话:

  • <link rel="subresource" href="jquery.js">: subresource 类型用于加载当前页面将使用 (但还未使用) 的资源 (预先载入缓存中),拥有较高优先级

  • <link rel="prefetch" href="http://NextPage.html">: prefetch 类型用于加载用户将会打开页面中使用到的资源,但优先级较低,也就意味着浏览器不做保证它能够加载到你指定的资源。

那么上一节延迟执行的方案就可以作为 subresource 与 prefeth 的回滚方案。同时还有其他的类型:

  • <link rel="dns-prefetch" href="//host_name_to_prefetch.com">: dns-prefetch 类型用于提前 dns 解析和缓存域名主机信息,以确保将来再请求同域名的资源时能够节省 dns 查找时间,比如我们可以看到淘宝首页就使用了这个类型的标签:

  • <link rel="prerender" href="http://example.org/index.html">: prerender 类型就比较霸道了,它告诉浏览器打开一个新的标签页 (但不可见) 来渲染指定页面,比如这个页面:

这也就意味着如果用户真的访问到该页面时,就会有“秒开”的用户体验。

但现实并非那么美好,首先你如何能预测用户打开的页面呢,这个功能更适合阅读或者论坛类型的网站,因为用户有很大的概率会往下翻页;要注意提前的渲染页面的网络请求和优先级和 GPU 使用权限优先级都比其他页面的要低,浏览器对提前渲染页面类型也有一定的要求,具体可以参考这里

利用 LocalStorage

在聊如何用它来解决我们遇到的问题之前,个人觉得首先应该聊聊它的优势和劣势。

Chris Heilmann 在文章There is no simple solution for local storage中指出了一些常见的 LS 劣势,比如同步时可能会阻塞页面的渲染、I/O 操作会引起不确定的延时、持久化机制会导致冗余的数据等。虽然 Chirs 在文章中用到了比如"terrible performance", "slow"等字眼,但却没有真正的指出究竟是具体的哪一项操作导致了性能的低下。

Nicholas C. Zakas 于是写了一篇针对该文的文章In defense of localStorage,从文章的名字就可以看出,Nicholas 想要捍卫 LS,毕竟它不是在上一文章中被描述的那样一无是处,不应该被抵制。

比较性能这种事情,应该看怎么比,和谁比。

就“读”数据而言,如果你把“从 LS 中读一个值”和“从 Object 对象中读一个属性”相比,是不公平的,前者是从硬盘里读,后者是从内存里读,就好比让汽车与飞机赛跑一样,有一个 benchmark 各位可以参考一下:localStorage vs. Objects:

跑分的标准是 OPS(operation per second),值当然是越高越好。你可能会注意到,在某个浏览器的对比列中,没有显示关于 LS 的红色列——这不是因为统计出错,而是因为 LS 的操作性能太差,跑分太低 (相对从 Object 中读取属性而言),所以无法显示在同一张表格内,如果你真的想看的话,可以给你看一张放大的版本:

这样以来你大概就知道两者在什么级别上了。

在浏览器中与 LS 最相近的机制莫过于 Cookie 了:Cookie 同样以 key-value 的形式进行存储,同样需要进行 I/O 操作,同样需要对不同的 tab 标签进行同步。同样有 benchmark 可以供我们进行参考:localStorage vs. Cookies

从 Brwoserscope 中提供的结果可以看出,就 Reading from cookie, Reading from localStorage getItem, Writing to cookie,Writing to localStorage property 四项操作而言,在不同浏览器不同平台,读和写的效率都不太相同,有的趋于一致,有的大相径庭。

甚至就 LS 自己而言,不同的存储方式和不同的读取方式也会产生效率方面的问题。有两个 benchmark 非常值得说明问题:

  1. localStorage-string-size

  2. localStorage String Size Retrieval

在第一个测试中,Nicholas 在 LS 中用四个 key 分别存储了 100 个字符,500 个字符,1000 个字符和 2000 个字符。测试分别读取不同长度字符的速度。结果是:读取速度与读取字符的长度无关

第二个测试用于测试读取 1000 个字符的速度,不同的是对照组是一次性读取 1000 个字符;而实验组是从 10 个 key 中 (每个 key 存储 100 个字符) 分 10 次读取。结论: 是分 10 此读取的速度会比一次性读取慢 90% 左右

LS 也并非没有痛点。大部分的 LS 都是基于同一个域名共享存储数据,所以当你在多个标签打开同一个域名下的站点时,必须面临一个同步的问题,当 A 标签想写入 LS 与 B 标签想从 LS 中读同时发生时,哪一个操作应该首先发生?为了保证数据的一致性,在读或者在写时 务必会把 LS 锁住 (甚至在操作系统安装的杀毒软件在扫描到该文件时,会暂时锁住该文件)。因为单线程的关系,在等待 LS I/O 操作的同时,UI 线程和 Javascript 也无法被执行。

但实际情况远比我们想象的复杂的多。为了提高读写的速度,某些浏览器 (比如火狐) 会在加载页面时就把该域名下 LS 数据加载入内存中,这么做的副作用是延迟了页面的加载速度。但如果不这么做而是在临时读写 LS 时再加载,同样有死锁浏览器的风险。并且把数据载入内存中也面临着将内存同步至硬盘的问题。

上面说到的这些问题大部分归咎于内部的实现,需要依赖浏览器开发者来改进。并且并非仅仅存在于 LS 中,相信在 IndexedDB、webSQL 甚至 Cookie 中也有类似的问题在发生。

实战开始

考虑到移动端网络环境的不稳定,为了避免网络延迟 (network latency),大部分网站的移动端站点会将体积庞大的类库存储于本地浏览器的 LS 中。但百度音乐将这个技术也应用到了 PC 端,他们将所依赖的 jQuery 类库存入 LS 中。用一段很简单的代码来保证对 jQuery 的正确载入。我们一起来看看这段代码。代码详解就书写在注释中了:

!function (globals, document) {
    var storagePrefix = "mbox_";
    globals.LocalJs = {
        require: function (file, callback) {
            /*
                如果无法使用 localstorage,则使用 document.write 把需要请求的脚本写在页面上 
                作为 fallback,使用 document.write 确保已经加载了所需要的类库 
            */

            if (!localStorage.getItem(storagePrefix + "jq")) {
                document.write('<script src="' + file + '" type="text/javascript"></script>');
                var self = this;

            /*
                并且 3s 后再请求一次,但这次请求的目的是为了获取 jquery 源码,写入 localstorage 中 (见下方的 _loadjs 函数)
                这次“一定”走缓存,不会发出多余的请求 
                为什么会延迟 3s 执行?为了确保通过 document.write 请求 jQuery 已经加载完成。但很明显 3s 也并非一个保险的数值 
                同时使用 document.write 也是出于需要故意阻塞的原因,而无法为其添加回调,所以延时 3s
            */
                setTimeout(function () {
                    self._loadJs(file, callback)
                }, 3e3)
            } else {
                // 如果可以使用 localstorage,则执行注入 
                this._reject(localStorage.getItem(storagePrefix + "jq"), callback)
            }
        },
        _loadJs: function (file, callback) {
            if (!file) {
                return false
            }
            var self = this;
            var xhr = new XMLHttpRequest;
            xhr.open("GET", file);
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        localStorage.setItem(storagePrefix + "jq", xhr.responseText)
                    } else {}
                }
            };
            xhr.send()
        },
        _reject: function (data, callback) {
            var el = document.createElement("script");
            el.type = "text/javascript";
            /*
                关于如何执行 LS 中的源码,我们有三种方式 
                1. eval
                2. new Function
                3. 在一段 script 标签中插入源码,再将该 script 标签插入页码中 

                关于这三种方式的执行效率,我们内部初步测试的结果是不同的浏览器下效率各不相同 
                参考一些 jsperf 上的测试,执行效率甚至和具体代码有关。
            */
            el.appendChild(document.createTextNode(data));
            document.getElementsByTagName("head")[0].appendChild(el);
            callback && callback()
        },
        isSupport: function () {
            return window.localStorage
        }
    }
}(window, document);
!
function () {
    var url = _GET_HASHMAP ? _GET_HASHMAP("/player/static/js/naga/common/jquery-1.7.2.js") : "/player/static/js/naga/common/jquery-1.7.2.js";
    url = url.replace(/^\/\/mu[0-9]*\.bdstatic\.com/g, "");
    LocalJs.require(url, function () {})
}(); 

因为桌面端的浏览器兼容性问题比移动端会严峻的多,所以大多数对 LS 利用属于“做加法”,或者“轻量级”的应用。最后一瞥不同站点在 PC 平台的对 LS 的使用情况:

  • 比如百度和 github 用 LS 记录用户的搜素行为,为了提供更好的搜索建议

  • Twitter 利用 LS 最主要的记录了与用户关联的信息 (截图自我的 Twitter 账号,因为关注者和被关注者的不同数据会有差异):
  • userAdjacencyList 表占 40,158 bytes,用于记录每个字关联的用户信息
  • userHash 表占 36,883 bytes,用于记录用户被关注的人信息

  • Google 利用 LS 记录了样式:
  • 天猫用 LS 记录了导航栏的 HTML 碎片代码:

总结

No silver bullet. 没有任何一项技术或者方案是万能的,虽然开源社区和浏览器厂商在提供给我们越来越丰富的资源,但并不意味着今后遇见的问题就会越来越少。相反,或许正因为多样性,和发展中技术的不完善,事情会变得更复杂,我们在选择时要权衡更多。我无意去推崇某一项解决方案,我想尽可能多的把这些方案与这些方案的厉害呈现给大家,毕竟不同人考虑问题的方面不同,业务需求不同。

还有一个问题是,本文描述的大部分技术都是针对现代浏览器而言,那么如何应对低端浏览器呢?

从百度统计这张 17 个月的浏览器市场份额图中可以看出 (当然可能因为不同站点的用户特征不同会导致使用的浏览器分布与上图有出入),我们最关心的 IE6 的市场份额一直是呈现的是下滑的趋势,目前已经降至几乎与 IE9 持平;而 IE9 在今年的市场份额也一直稳步上升;IE7 已经被遥遥甩在身后。领头的 IE8 与 Chrome 明显让我们感受到有足够的信心去尝试新的技术。还等什么,行动起来吧!

其他参考文献


感谢王保平对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论