Js 问题分析--js 影响页面性能
现状分析:问题陈述
分析问题:抽象问题根源,通过实例或推理证明问题的严重性
问题引申:以现有问题为点开始扩散,这将导致其它什么问题,或同一类型的问题
问题总结:从分散开始回归,再次抽象问题
5.1 DOM 操作不当影响页面性能
现状分析:
我们的页面上对 DOM 的操作在所难免,无论是焦点图切换这样的交互效果,还是各种
数据接口的应用,DOM 操作都是重要的环节。目前页面上的 JavaScript 越来越多,一旦我们
的 DOM 操作不当,必然对页面产生严重的性能问题。比如:电脑网报价库左树、appendChild
插入 js 的方式,都存在 DOM 操作阻塞整个页面渲染的问题。
分析问题:
DOM 是文档对象模型,最主要的 web 前端应用程序接口,是一个独立于语言的,使用
XML 和 HTML 文档操作的应用程序接口。
DOM 在浏览器中的接口是通过 JavaScript 实现的,客户端大多数脚本程序都需要与文档
打交道,使得 DOM 成为 JavaScript 代码日常行为中重要的组成部分。
浏览器通常要求 DOM 实现和 JavaScript 实现保持相互独立。
例如,在 Internet Explorer 中,被称为 JScript 的 JavaScript 实现位于库文件 jscript.dll 中,而
DOM 实现位于另一个库 mshtml.dll(内部代号 Trident)。这种分离技术允许其他技术和语言,如 VBScript,
受益于 Trident 所提供的 DOM 功能和渲染功能。Safari 使用 WebKit 的 WebCore 处理 DOM 和渲染,具有一个
分离的 JavaScriptCore 引擎(最新版本中的绰号是 SquirrelFish)。Google Chrome 也使用 WebKit 的 WebCore
库渲染页面,但实现了自己的 JavaScript 引擎 V8。在 Firefox 中,JavaScript 实现采用 Spider-Monkey(最
新版中称作 TraceMonkey),与其 Gecko 渲染引擎相分离。
摘自《High Performance JavaScript》
两个独立的部分以功能接口连接就会带来性能损耗,这意味着每次访问 DOM 都需要付
出一定代价,你可以把 DOM 想象成一个岛屿,把 JavaScript 想象成另一个岛屿,两个岛屿有
一座桥连接,每次过桥都需要支付一定的过桥费,可以想到,你访问 DOM 岛次数越多,所
支付的过桥费也就越多,所以为了减少开销,应该减少访问 DOM 的次数,尽量停留在JavaScript 岛上。
Eg:http://zzb.pcauto.com.cn/power/js/jsProblem/dom.html
问题引申:
1.HTML 集合:
HTML 元素的集合,例如 document.getElementsByTagName、childNodes 等得到的都是
HTML 集合,虽然 HTML 集合也有 length 属性,但它不是数组(因为它们没有诸如 push()
或 slice()之类的方法),访问 HTML 集合的 length 比数组的 length 要慢。
遍历数组比遍历集合快,如果先将集合元素拷贝到数组,访问它们的属性将更快
实在不能转换成数组,可以将集合的 length 缓存起来,避免多次遍历集合
http://zzb.pcauto.com.cn/power/js/jsProblem/dom2.html
2.循环:
循环向来是程序效率的重中之重,不当的循环会将程序的性能问题放大很多,对于 DOM
操作来说,本来这样的操作就已经先天不足了,如果再放到某些循环中,带来的影响将会是
灾难性的。
尽量使用局部变量来缓存 DOM,避免在循环中访问 DOM 岛。
function toArray(coll) {
for (var i = 0, a = [], len = coll.length; i < len; i++) {
a[i] = coll[i];
}
return a;
}
var coll=document.getElementsByTagName("div");
for(var i=0,len=coll.length;i<len;i++){
....
}问题总结:
DOM 是一个独立存在的 API,独立使得其可以提供方便的接口得以操作 DOM,当然这
是以性能为代价换来的。在浏览器中,DOM 提供接口给 JavaScript 用于操作 xhtml 文档,
JavaScript 解释型语言的特性加上 DOM 的独立性,使得操作 DOM 很容易造成页面的性能瓶
颈。
5.2 首屏 js 阻塞页面
http://www.gethifi.com/tools/regex
http://zzb.pcauto.com.cn/tools/reg/reg.html
现状分析:
无论是从空白页面新打开一个地址,还是点击一个链接打开新页面,页面首屏的显示快
慢直接决定了用户的第一感受。而在众多决定首屏显示时间的因素中,js 带来的阻塞无疑是
最为严重的。由于浏览器本身的工作原理,在解析 html 文档时是至上而下的解析,且 js 在调
用前需要预先定义,在 html 中的体现就是很多 js 函数的定义代码会直接放在页面头部的 head
标签内。例如:ssi 合并 js(含 jquery)、头部导航、广告、快搜接口、功能函数、ivy
头部引用 js 是为了页面上的调用能够生效
分析问题:
Js 阻塞页面分为两种情况:js 阻塞渲染和 js 阻塞其他请求
for(var i=0;i<10;i++){
document.getElementsByTagName("div")[i].className="style"+i;
}
var divs=document.getElementsByTagName("div");
for(var i=0;i<10;i++){
divs[i].className="style"+i;
}
<script src="http://www.pconline.com.cn/ssi/js/channel/index.html"
type="text/javascript"></script>
<base target="_blank" />
</head>Js 阻塞渲染:
Eg:http://zzb.pcauto.com.cn/power/js/jsProblem/jsblock.html
在浏览器中,js 脚本和 DOM 渲染分别由两个独立的模块来完成,而浏览器本身是单线
程工作的,也就是说,浏览器在同一时间只能调用其中的一个模块来工作,而必须暂停另外
一个。这样的工作原理使得 js 的执行对页面的渲染造成了阻塞。
图 5.2.1 浏览器不能同时进行 js 执行和 DOM 渲染
事实上,任何地方的 js 都会阻塞页面,使页面渲染停止,只是从用户体验的角度来说,
首屏内的 js 造成的阻塞会让用户感觉更加明显。其次,js 的执行有可能会带来对页面的修改,
在不清楚页面将会作何修改的时候暂停渲染,也是浏览器出于效率上的考虑。
图 5.2.2 Js 的执行同样会阻塞页面
Js 阻塞其他请求:
除了异步的 js 请求,一般的 js 请求不会和其他请求并发(用 document.write 可以让 js 和
js 并发),换句话说,此时浏览器不会发出其他请求,浏览器此时也不会继续解析 html 代码,
必须等到 js 接收完毕,并执行完毕,html 的解析才会继续
图 5.2.3 Js 阻塞页面
问题引申:
1.js 代码在 html 中的位置
由于浏览器至上而下解析 html 的工作原理,为了减少 js 对渲染的影响,应该让 js 代码
越靠近页面的底部越好,但是这样会带来两个问题:
一、将所有 js 都放在页面的最后,必然会在页面渲染完成时占用很多的时间去执行和加
载 js,此时浏览器进度条将会呈现一直运转的状态,而页面也将呈现“假死”(无法响应用户
的操作)。
二、将所有 js 都放在页面的最后,一些元素的事件绑定将会被推后,即在加载过程中,虽然页面已经完成渲染和显示,但用户的操作会无效。
由此看出,如何在 html 中放置 js 代码,让 js 能够平稳加载并执行,让页面能够渐进的
过渡渲染,将会是另一个值得探讨的问题。
2.页面的首屏时间
基调网络的首屏时间定义为:
据基调介绍,基调是检测 800x600 范围内的 8 个点的显示情况来判定首屏是否显示的,
这是一种相对感性的方法,目的是为了最大程度的贴近用户真实的感受,而实际操作中,这
样的时间点却是受到诸多因素影响,无法用技术的手段去测试或模拟得出准确的结果。例如:
在首屏结束位置插入一张 1x1 大小的 gif 图片来标记首屏时间,如果页面解析很快,这张小
图的请求就会很快发出,而此时首屏的内容有可能还在加载或是渲染中,所以这种方法并不
能反映最真实的情况。
图 5.2.4 基调的工具可以大概确定首屏时间,但用其他技术手段并不好确定
IE 浏览器显示第一屏主页面的消耗时间。
首屏的定义以 800X600 像素尺寸为标准。从开始监测开始计时,到 IE 浏览器页面显
示高度达到 600 像素且此区域有内容显示之后的时间。问题总结:
Js 在 web 前端应用变得日益重要,越来越多的应用构建在 js 上,js 一方面能给网页带来
丰富、便捷的体验,另一方面也带来了效率上的问题。
在 js 阻塞页面渲染上主要有以下两个方面:
一方面,浏览器单线程的工作原理,使得 js 对页面产生的阻塞在所难免,加上 ie6 这样
的低效浏览器仍然是市场主流,阻塞带来的问题在现阶段显得更加明显;另一方面,经过测
试有望找到合理安排 js 的方式,实现 js 的渐进过渡,最大程度减轻 js 对页面的阻塞,并且随
着浏览器的不断升级与用户硬件设备的不断提升,也有助于减少 js 带来的阻塞。
5.3 异步 js 执行时间不确定
现状分析:
先说说异步和同步的概念:
异步是相对于同步而言的,同步并不是指同时进行活动,而是指协同、配合的活动。我
们说 A 和 B 同步,是指 A 执行到一定程度时要依靠 B 的某个结果,于是停下来,示意 B 运
行;B 执行,再将结果给 A;A 再继续操作。
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能
调用这个方法。
摘自网络
而异步则恰恰相反,即 A 和 B 的活动互不影响、互不制约,在 A 活动时,B 也可以执
行。
在 web 中的同步异步往往是指客户端和服务器端通信的过程,传统的过程就是同步的:图 5.3.1 Web 中的同步和异步模型
目前异步 js 主要由以下两种方式实现:
1. ajax:不能跨域,依赖 xmlhttprequest
2. appendChild(scriptDom):可以跨域,不阻塞其后的请求
由于 ajax 在跨域上的缺陷,现在用得较多的是 appendChild (scriptDom)的形式,如 needJs
和 defineJS 中的异步调用都是以 appendChild(scriptDom)作为核心方法。
为了最大程度减少 js 的请求对其后请求的阻塞,我们的网站中正逐步推行“按需加载、
异步请求”的模式来对待外链的 js,了解异步 js 的执行时间和特点显得愈发重要。
分析问题:
异步 js 的执行时间在各浏览器中有不同的表现:
Eg:http://zzb.pcauto.com.cn/power/js/jsProblem/asynJs/asynJs.html
asynJs.html 使用 appendChild 方式异步请求 wirte1 和 write2 两个 js,write1 体积明显大于
write2,且 write1 先请求。结果,各浏览器下的执行顺序略有不同:
ie、chrome:谁先返回谁先执行(在没有缓存的情况下,write2 会比 write1 先返回,故
可以看到 write2 先执行);
FF:谁先请求谁先执行既然 js 的执行顺序会有区别,如果两个异步请求的 js 有依赖关系则会引起浏览器错误:
Eg:http://zzb.pcauto.com.cn/power/js/jsProblem/asynJs/asynJs2.html (ie、chrome 下 ctrl+F5
会出错)
图 5.3.2 ie 下 write3 先请求,后返回,后执行
问题引申:
1.异步请求
如果发出异步请求的脚本块 A 本身执行的时间很长,长到异步请求的脚本 B 已经返回还
未执行完,这时浏览器是会继续执行原来的 A,还是中断 A,转为执行脚本 B 呢?为此,我
们增加脚本执行的时间:
Eg:http://zzb.pcauto.com.cn/power/js/jsProblem/asynJs/asynJsCut/asynJsCut.html
Ie8 页面显示(FF、chrome 执行顺序一致,时间稍有不同):
可以看到,waitTime1 执行完后执行了 waitTime2,最后才执行 t1。从 dynatrace 的跟踪也
可以看到,t1.js 在 script 脚本还未执行完时就已经接收完毕(图 5.3.3 箭头所示棕色位置),
而此时并没有立刻执行 t1.js,图 5.3.4 中显示,t1.js 是在 script 脚本块执行完后才执行的。所
以,对于异步请求的 js 接收完后,并不会立刻执行,而会排队待 js 引擎空闲后再执行。这
一点和 setTimeout 的机制有些类似。
waitTime1 完成:500 毫秒
waitTime2 完成:1003 毫秒
js1 执行 1088 毫秒
t1.js 载入完成 从开始到载入完成花费:1089 毫秒图 5.3.3
图 5.3.4 异步 js 返回后并没有立即执行
2.script 脚本块、js 定时器
Js 的执行是按脚本块为单位一个一个来执行的。在遇到一个脚本块时,js 引擎会把这个
脚本块丢到脚本队列(也叫调用堆栈)中去排队执行,一旦当前 js 引擎出于空闲状态,就会
执行脚本队列里的脚本块,先入队列的脚本块会先执行。
由于 js 为单线程执行,所以每次只能执行一个脚本块,这时会阻塞其他异步 js 的执行(如
鼠标单机事件,定时器触发,或者异步请求完成),这些异步 js 同样被安排到脚本队列中等
待 js 引擎的空闲。
setTimeout 并不是严格意义上的定时器,而是将 js 在指定时间后安排到脚本队列的一种
方式,而实际执行该 js 的时间则要看脚本队列中的情况以及 js 引擎的空闲状态来决定。另外,
如何将 js 安排到脚本队列,各个浏览器存在差异。看下面的例子:
http://zzb.pcauto.com.cn/power/js/jsProblem/asynJs/setTimeoutJsBlock.html
http://zzb.pcauto.com.cn/power/js/jsProblem/asynJs/setTimeoutJsBlock2.html
第一个例子,FF 下 setTimeout 的 b 偶尔会在 c 前执行,ie8 下即使延时增加,b 也无法在
c 前执行。
第二个例子,在不延时的情况下,FF 和 ie8 都会将 b 的执行安排到最后。(ie6 和 FF 类
似)
结论:setTimeout 执行的 js 和内嵌的 js 在执行上的顺序无法保证。
问题总结:
由异步 js 执行时间的不确定,引出的是对 js 异步执行机制的思考。通过以上的分析可以
初步得出一下结论:
1.异步 js 的执行主要有以下几类:用户触发的事件处理(鼠标点击、滚动页面等)、异步
请求的 js、定时器触发。
2.js 引擎是单线程的
3.异步 js 会进入 js 调用堆栈中排队执行4.各浏览器对于异步 js 安排到堆栈中的处理方式存在区别,表现出来就是在异步请求 js
以及定时器触发 js 在执行顺序上的不同
5.4 defineJS 的使用
现状分析:
为什么要用 defineJS?
客观因素:
浏览器单线程的本质决定了以下两个问题:
1.js 的请求阻塞其后的请求(浏览器不知道 js 会做什么操作,所以在请求 js 的时候不会
请求其他文件,说不定此时 js 会有跳转操作呢。PS:这里指的 js 请求是<script src=””>这样
的标签形式)
2.js 的执行阻塞页面的渲染(由单线程本质决定,具体查阅 5.2 的内容)
这样两个问题使得 js 成为了页面效率的最大杀手,为了保证首屏的内容能够最快的显示,
让用户得到最快的响应,排除阻塞因素、延迟 js 成为了最直观的方法, defineJS 由此应运而
生。
现实情况:
SSI 合并 js 的做法虽然将多个 js 的请求合并成了一个,却在无意识中增加了页面 js 的冗
余度。高耦合在使用上带来了方便,也带来了冗余和笨重,加上 js 对浏览器的特殊影响,使
得在这个问题的权衡上,SSI 的做法是弊大于利的。
打破 js 冗余带来的不利影响,让 js 的调用更加灵活、准确,也是 defineJS 的另一使命。
defineJS 能做什么?
简单来说
defineJS 可以延迟 js 的请求。
defineJS 可以减少 js 的冗余。
defineJS 可以将内嵌 js 对外部 js 的依赖关系控制得更加灵活、准确。
分析问题
defineJS 参数详解:图 5.4.1 defineJS 的参数说明
defineJS 流程详解:
图 5.4.2 代理机制和回调函数是 defineJS 的核心。
第一步:声明对需要 defineJS 的函数进行初始化参数配置,如:
defineJS(„usejquery,!$,!jQuery,!$.getScript=/www1/js/pc.jquery1.4.js‟);
具体参数配置可以参照参数详解。
(ps:对于对象的多级声明,层级越高放越后,比如:$.getJSON 在声明时需放在$后,即:
defineJS(“$,$.getJSON=jquery.js”)
)
第二步:格式化参数
这一步会对参数做一下处理:
1.
defineJS(„usejquery,!$,!jQuery,!$.getScript=/www1/js/pc.jquery1.4.js‟);
会在格式化参数步骤中分解成完全等效的
defineJS("usejquery =/www1/js/pc.jquery1.4.js ");
defineJS("!$=/www1/js/pc.jquery1.4.js ");
defineJS("!jQuery =/www1/js/pc.jquery1.4.js ");
defineJS("!$.getScript =/www1/js/pc.jquery1.4.js ");
2.通过!判断是异步还是同步,如果是同步,url 是否符合规则
3.是否需要延时处理
4.函数是否已经加载
第三步:设置代理函数
这里简要说下代理机制的原理:
以(„fn=fn.js‟)为例:
代理机制会定义一个名字同样为 fn 的函数(这里为了防止混淆,用 fndl 标识,实际上名字
还是 fn),当第一次调用 fn(arg)时,实际上是调用了 fndl(arg),而 fndl 将 arg 保存,在回调
函数 ok()中将 arg 参数推入到刚加载的真实函数 fn 中。事实上,fndl 起到了保存 arg、转告
arg 的作用。打个比方:这里有 boss、小秘、打工仔三个人,boss 有些材料要叫打工仔去买,但是打工
仔外出了,于是 boss 找来小秘,让小秘等打工仔回来时转告他要去购买的材料,并交了一
份购买清单(arg 参数)给小秘,boss 就去旅游了,小秘打了个电话急招打工仔回来,等了
一会,打工仔回来了,小秘第一时间将 boss 交待的事转告给了打工仔,打工仔接到了清单,
立马出发,不到一会就将所有材料买回。
这里的小秘就是打工仔的代理函数,她起到了保存清单和转交清单的作用。
第四步:加载脚本
具体看下一小节。
第五步:加载完毕,触发 ok()
当 js 加载完毕后,新的 fn 函数会覆盖掉代理 fn,并通过 ok 把原参数推到新的 fn 函数中调
用。
defineJS 同步和异步如何选择:
在 defineJS 的参数中,如果函数名前加上了“!”号,代表此函数调用时,请求 js 用同步
模式,这样做会使得页面的解析和渲染、甚至其后的请求都收到阻塞。那为什么还要在
defineJS 中提供同步模式呢?
原因在于异步模式发出 js 请求后,主程序会继续执行,此时请求的 js 还未返回,如果主
程序有立刻调用依赖外部js的函数时,就会因为未定义而报错。具体可以看下面例子中的yb2:
eg.http://zzb.pcauto.com.cn/power/js/jsProblem/defineJS/defineJSty.html
而同步模式用到了 ajax 的同步机制,当请求发出后,主程序会挂起等待 js 的返回,此时
主程序不会继续执行,避免了其后调用还未定义的函数。待 js 返回后才会继续执行主程序。
可以看上面的例子中的 tb 调用。
因此,如果行内脚本中存在依赖外部脚本返回值的代码,就需要用同步模式。
例如:jquery 的链式调用:
又如:new 对象的操作:
$(“#id”).hide();//在调用$时,会触发 jquery 的请求,如果此时不用同步模
式暂停主程序,程序会继续执行 hide 函数,而 hide 函数在 jquery 请求返回之前是未
定义的,所以会报错。当然,如果对于 jquery 这类调用一定要使用异步模式也是有办法的:
1.这需要在请求的 js 中加入一个“外壳函数”,在外壳函数中调用 js 所提供的方法
以 jquery 为例:
外壳函数:
2.defineJS 的申明函数换成这个外壳函数
3.实际调用时的代码也需放到外壳函数中
可以查看 eg.http://zzb.pcauto.com.cn/power/js/jsProblem/defineJS/defineJSty.html
中的 yb3
问题引申
defineJS 存在的弊端:
1.对于同步模式请求的 js 需使用相对路径,一些动态应用使用 defineJS 需要在动态本地
存放 js,而不能整站公用。
2.defineJS 需在头部预先定义,且嵌入到 html 页面上,如果后期需要更新维护,需要逐
个修改页面。
3.defineJS 同步模式由于使用 xmlhttprequest 发出请求,而 xmlhttprequest 对于不能指定文
档格式的文件,如:js,默认使用 utf-8 编码。而我们的 js 一般都使用 gb2312 格式,且中文
在 utf-8 和 gb2312 中编码不同,(英文和特殊字符相同),所以,同步加载的 js 中不应含有中
文,如果一定要加入中文,则需将 js 转为 utf-8 编码。这也是 pc 的 jquery 中含有下面这句话
的原因:
4.延迟加载的时间无法精确控制。defineJS 请求的时机是由浏览器的解析决定的,当解析
usejquery(function($){
$(xx).xxx;
})
defineJS(“usejquery=jquery.js”);
function usejquery(fn){
fn(jQuery);
}
defineJS(“slidebox=slide.js”);
var a=new slidebox();//异步调用时,执行到 slidebox()时会发出请求,此时
js 还未返回,a 的值已经不是期望得到的对象了
a.slide();//调用 a 的 slide 方法,而 a 并未成为 slidebox 的对象,slide 方法
不存在,报错”速度很快,请求有可能会在某些请求前发出,这主要是由于浏览器的解析和渲染无法绝对一
致所导致。当浏览器解析完首屏的代码时,往往首屏还未渲染完毕。
defineJS 语句分析:
1.
function(fn){
if(fn)return callback(false);
}//fn 是否定义,fn 需作为参数,如果未定义,参数会为 undefined,而在函数外如果未定
义,会报错
2.scripts[src]={
onload:false,
callbacks:[]
}//把地址作为子对象名
3.if(arr.push(a)==1)//数组 push 后返回数组长度
4.初始值、初始表达式
var a=b||"default";
var a=b||(b="default");//b 的值会作为表达式的值返回给 a
5.function alert(s){
window.defineJSlog+=”\n”+s;
}//记录调试信息,不干扰用户,可在地址栏用 javascript:alert(defineJSlog)查看
6.fn.toString().indexOf(“defineJSlog”)//查找 fn 的定义代码中是否含有“defineJSlog”字样
7.eval.call(window,”fn”)//指定在 window 作用域下执行 fn,在 js 中 eval 和 with 可以动态
修改作用域
问题总结:
defineJS 是结合了无阻塞和按需加载两种优化思路的 js 加载方案,虽然还存在同步、预
先定义、相对路径等问题,但在实际的使用过程中,defineJS 确实发挥了不错的效果,使得
页面在 js 加载上能够更加合理和规范,这也是对前端性能以及资源利用的一次大胆尝试,而
其中涉及到的很多方法和思路,也是了解 javascript 底层原理的很好的实例。5.5 页面内容的浪费加载
现状分析
随着互联网的高速发展和全民上网环境的不断提速,web 网站的数量和复杂程度都呈现
出爆炸式增长的趋势。我们在越来越多的网站上看到,Web 页面承载着难以置信的信息量。
在门户网站首页,多达四、五屏的页面已经习以为常,滚动滚轮似乎成了打开每个页面
都必做的操作。而这仅仅是可见的信息量,越来越多的页面将信息隐藏、堆叠起来,即使你
看不到它,即使你不需要它,但是它的确在那里,它的确从服务器传输到了客户端,更重要
的是,它的确占用了带宽,换句话说,它是有成本的。
如何在不影响用户的操作前提下,最大化的节约消耗和成本,已经成为另一个值得研究
的前端领域。
问题分析:
何为浪费加载:
首先,目前的 web 页面都基于浏览器,而浏览器可视区域的大小受到显示设备的限制,
所以,超出视窗的内容只有通过滚动条才能够看得到。换句话说,如果一个页面有五屏,而
你的用户在开完第一屏后就关闭了浏览器,那么,第二屏到第五屏的数据就属于浪费加载的
范畴。
其次,现在的页面有很多隐藏的内容,默认情况下是不显示出来的,除非用户进行了某
些操作,比如点击或移上某个元素,才会触发显示。同样,如果用户并未进行这样的操作,
这些内容也属于浪费加载的范畴。
图 5.5.1 用户未切换的选项卡属于浪费加载的范畴
原则“可见的才是需要的”
面对页面上的众多信息,最大化信息利用率的原则就是“可见的才是需要的”,换句话说,
当前不可见的就不需要加载、不需要显示,这也是近来提及比较多的“按需加载”。从传统的
将信息全部推送给用户,到让用户自己选择需要的信息,“按需加载”事实上是把更多的权利
交给了用户。不过,按需加载也是有利有弊的:
利:
1. 减少页面的浪费加载,节省不必要的带宽
2. 加快页面的载入时间,减少用户的等待
弊:
1. 按需加载的内容基本丧失了搜索引擎抓取的可能,会带来一定流量的减少
2. 按需加载在触发时才加载数据,用户会感觉有一定延迟
3. 如果没有合理的选择按需加载触发的时机,往往会带来不好的体验
有谁在使用“按需加载”
1.yahoo
作为世界级的门户网站,yahoo 对于前端的关注在业内是众所周知的,YUI 也一直是前
端行业的风向标。Yahoo 首页对于按需加载的使用可谓发挥到了极致,总的页面只有一屏半
的高度,庞大的各类内容都放在了左侧的导航上进行按需加载。这样的处理,使得 yahoo 首
页的加载速度极快,左侧还提供了定制功能,方便用户定制感兴趣的信息。
图 5.5.2 Yahoo 首页左侧大量用了“按需加载”
加载方式:发出请求加载,appendChild 插入
2.淘宝
淘宝不仅是国内最成功的 C2C 平台,其在前端领域的实力也在国内名列前茅。
淘宝对于按需加载的使用主要在首页切换卡和产品列表页上:
首页的切换卡中的内容用<textarea>包住,这样可以避免浏览器解析其中的代码;图 5.5.3 淘宝首页选项卡内容用了 textarea
而产品列表页则没有给视窗外的图片设置 src值,而是把图片的地址写在 data-lazyload-src
上,避免请求图片。
图 5.5.4 列表页图片用 data-lazyload-src 代替 src
3.QQ
首页采取的是和 yahoo 相同的形式,即异步请求加载,不同的是,qq 请求返回的是一个
html 页面,而 yahoo 则是返回一个 json 数据。从这点来看,qq 的方式更加直观,便于维护。
而在图片方面,qq 的做法和淘宝类似,也是用自定义属性代替图片 src 的方法:
图 5.5.3 qq 图片延迟方式
4.sina
作为国内门户三强之一,sina 的信息量可谓巨大,不过实际发现其并未在首页使用按需
加载,而发现在博客的文章页中对图片进行了按需加载的处理:
Sina 的做法是给所有图片默认设置了 src 为一张 1x1 的小图,真实的地址则用自定义属
性,究其这样做的原因,应该是博客内页的图片大多由网友上传,大小不一,先用小图占大
图的位,这样做可以让按需加载的图片,在加载完成时不改变文章内容的布局,避免给用户
带来不便。而前面提到的淘宝和 sina 的图片都是尺寸统一固定的,用样式进行统一控制就可
以了。
图 5.5.4 sina 博客文章页图片延迟(图片有设宽高)
按需加载的分类
按加载的内容来分可以分为两类:
1.代码
按需加载代码包括 html 的所有代码,在未加载之前,html 代码不会被解析,其中的图片
也不会请求,主要有以下几种实现方式:textarea、异步请求、iframe
①<textarea>:用 textarea 标签包住需要按需加载的 html 代码,当加载事件触发时,就
将 html 的代码传给指定元素的 innerHTML 即可,此时,浏览器才会解析这一段 html 代码,
如果这段 html 中含有图片,此时才会发出图片请求。优点:操作简单,容易维护,响应迅速
缺点:textarea 需要绑定在页面上,需改动原页面代码,textarea 中的内容不会被搜索引
擎抓取到。
②异步请求:此方式是指当触发事件后,通过 httprequest 或者 DOMScript 请求数据,服
务器返回 json 格式或者 html 片段,再利用回调函数更新页面。Json 格式的数据小巧灵活,可
根据页面的需求生成不同的 html,适合各种动态接口使用;html 片段的方式看起来更加直观,
便于理解,也免去了在客户端重新组装 html 的过程,适合代码量较大、形式较为固定的情况。
优点:可在多个页面使用,加载的内容和页面分离,易于管理
缺点:请求返回会花费一定时间,无法做到立即刷新内容
③iframe:需要加载的内容放在另一个页面中,触发事件时,用 js 动态插入 iframe,
此时,浏览器才开始请求 iframe 中的内容,以此达到按需加载。
优点:iframe 的页面相对独立,主页面不受 iframe 的样式影响。
缺点:会受到 iframe 的各种限制
2.图片
图片是网页中的重要组成部分,其占用的带宽相对于文本也要大得多,所以在节约带宽
上,图片按需加载往往起到更为重要的作用。
针对图片的按需加载主要应用在图片列表页和文章页中,主要的方式是用自定义属性来
保存真实的图片地址,当需要加载时才将图片地址赋值给 src 属性。这样做的原理是图片只
有当 src 有值时,浏览器才会发出图片请求。
问题引申:
事实上,按需加载的难点并不在技术,其难点在于选择使用的时机。什么时候该使用按
需加载,什么时候改用什么方式,这才是最难的。想要节约带宽,前提是不能降低用户体验
或者减少访问量。“天下没有白吃的午餐”正是这个道理。
从用户体验上来说,按需加载是基于 web 的人机交互的一种新的方式,它的核心还是人。
一切从人的角度出发,基于人的感受,提供最人性化的服务,这将是按需加载带给我们的新
的思考。这样人性化的操作还表现在图片浏览的预加载上:预先加载后一页的图片内容,减
少用户翻页时等待的时间,这也是人性化优化的另一典范。
问题总结:
按需加载在一定程度上可以节约带宽,并减少页面内容的浪费,如果使用得当,还可以
减少用户等待时间,提高用户体验。但是对于 SEO 要求比较高的页面,按需加载就显得力不
从心了;另外这样的做法需要对页面的代码做响应的调整,如果大量使用还需考虑推广实施
的问题,比如用置标将 img 的 src 属性自动替换等等,尽量减少对正常代码的修改。5.6 Js 外链与内嵌的选择
现状分析:
Javascript 在 html 中的存在形式有两种:一是<script>xxxx</script>脚本块,二是<script
src=”xx.js”></script>外链 js 文件,也即是通常说的内嵌和外链两种形式。
查看各个知名站点的源代码可以发现,这两种形式都常常出现,他们散乱的位于各种 html
标签中间,让原本优雅的 html 代码多了几分“瑕疵”。的确,由于 script 的特殊性,使得它
在 html 中总会是最为显眼的,而 script 在页面中的位置和存在方式也是值得关注和重视的地
方。
问题分析:
Script 的存在方式
内嵌:
用<script>和</script>包裹住的 javascript 代码块。当浏览器解析到这样的代码时,会以
script 块为单位,将其扔给相应的 js 引擎去执行,并暂停等待 js 引擎的结果,待 js 引擎执行
完毕后,才继续后续的 html 的解析。
外链:
在 script 标签中,用 src 指定请求的外链的 js 地址。当浏览器解析到这样的代码时,会
请求指定的 js,此时浏览器暂停继续解析页面,待 js 返回后,开始执行请求的 js,在执行后
才继续页面 html 的解析。
此外,外链 js 也可以通过异步请求的方式加载,此时的执行顺序在不同浏览器中会存在
差异,具体可以查阅《异步 js 执行时间不确定》一节
内嵌与外链的多项对比
维护:
内嵌的 js 由于是放在 html 页面上的,所以对于内嵌 js 的维护需要涉及到响应的页面,
在大批量进行 js 修改时,内嵌 js 会很不方便;外链 js 由于从 html 页面独立出来,内容只有
一份,相应的维护也只需要修改一次即可。
效率:
外链 js 由于需要浏览器请求,并等待服务器返回 js 文件(正常 js 的请求还会阻塞其他请
求),所以,外链 js 的实际使用效率要比内嵌 js 差一些。
缓存:内嵌 js 的体积是直接算在 html 页面上的,且一般的 html 不会做缓存的操作,也就相当
于内嵌 js 是没有缓存的;而外链 js 则可以根据其使用特点,进行缓存的设置,以此减少流量
的消耗。
执行顺序:
在执行顺序上,内嵌的 js 在各浏览器中可以保证完全按照在 html 中出现的顺序来执行;
而外链的 js 如果是异步请求,则无法在所有浏览器中都保证执行顺序。在之前的《异步 js 执
行时间不确定》一节中有说道,这里看一下另外一种外链 js 执行顺序出问题的例子。
Eg: http://zzb.pcauto.com.cn/power/js/jsProblem/out/script2.html
FF 下的结果为:
第一行
第二行
Ie 下的结果为:
第二行
第一行
目前分析是由于 ie 在执行一段 js 的过程中是不能够中途中断去请求其他 js 的,只有等
到执行完了或者说 js 引擎空闲了,js 的请求才会发出;而 ff 一旦有 js 请求,就会中断 js 执
行。(游戏网头部的广告也是这种情况)
问题引申:
事实上,js 的外链和内嵌的选择和 css 的外链和内嵌的选择有相似的地方,比如维护成
本、效率、缓存方面的考虑,不太一样的是, 外链的 css 并非阻塞元素,所以一般不会对 css
做异步请求的处理,也就不会出现在解析顺序上不一致的问题。此外,css 本身就有优先级的
控制机制,例如 id 优于 class 等,使得 css 的解析顺序能够得到很好的控制。
问题总结:
Js 的内嵌和外链没有绝对的优劣,如何选择需要按照实际情况来分析,并综合考虑维护、
效率、缓存、执行顺序等多方面因素。