一: Web性能提升14条规则:
在做性能优化时,不要浪费时间去尝试为那些不消耗大量时间的代码提速。
评估优先,拒绝任何不能提供良好效益的优化。
高性能网站“足够快”的评估准则:
二:创建快速响应的Web应用
别把运行时间可能会很长的低性能代码引入到网页中。
然而, 有时执行任务的开销非常高,且无法神奇地把它优化得耗时更少。这种导致用户界面出现糟糕停滞的情形无法避免吗?就没有一个能让用户顺利执行的解决方案吗?
在这种情况下,传统的解决方案是使用多线程来把开销很大的代码从与用户交互的线程中剥离开来。然而,JavaScript并不支持多线程。
JS引擎是单线程的: https://blog.csdn.net/HuangsTing/article/details/111830927
因为,多线程在各个方面违反了抽象概念,主要是产生了竞争状态,死锁的风险和悲观锁定开销,并且它们无法横向扩展去处理未来超级内核的亿万次计算能力。
简单来说,就是不同的线程可以访问并修改相同的变量。但出现A线程要修改B线程正在修改的变量或类似情况时,这会导致各种各样的问题。
我们需要的是一种像多线程那样能多任务并发执行,却没有线程之间相互侵入危险的方法。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。(所以,这个新标准并没有改变 JavaScript 单线程的本质)
在页面上任何开销可能很大的,(例如,长时间运行的)JavaScript操作都应该委托给Worker。
主要有以下2点:
优化内存的方法:
三:拆分初始化负载
web应用程序很大一部分的应用代码不会在启动时被使用。
为了能尽可能快速渲染出网页,可以把JavaScript代码分为两部分:
注意:拆分JavaScript代码要避免出现未定义标识符错误。(在JavaScript执行时引用到一个被降级到延迟加载的标志符时,就会出现这种问题)
解决方式:
● 可以通过改变元素的展现来解决此问题。如,显示“加载中…”的图标。
● 在延迟加载的代码里绑定界面元素的事件处理程序。(例如,页面中的下拉菜单被初始化为一个静态文本,点击它不会执行任何JavaScript。延迟加载的代码包括菜单的功能和事件的绑定,通过attacEvent或addEventListener来实现)
使用桩(stub)函数来解决。
● 桩函数是一个与原函数名称相同但是函数体为空,或是用一些临时代码代替原有内容的函数。在初始化下载的代码中插入桩函数,当调用它们时,动态加载其他的JavaScript代码。当新增的JavaScript代码下载完成,原函数会覆盖桩函数。
● 可以给每一个被引用但又被降级为延迟下载的函数创建一个桩函数。桩函数可以返回一个桩值,例如一个字符串。稍微高级一点的使用,可以用桩函数记录用户的请求,并在JavaScript完成加载时调用相应的代码。
除了拆分JavaScript,拆分CSS样式表也是有益的。不过相对于拆分JavaScript,后者节省的资源要少一些,并且样式表整体大小通常比JavaScript小,且下载CSS并不会像JavaScript那样具有阻塞特性。
四:无阻塞加载脚本
script标签的阻塞行为会对页面性能产生负面的影响。但有时候,这种阻塞是必要的。因此,能够识别出JavaScript不依赖于页面中其他内容而单独加载的情况,是非常重要的。
JavaScript以行内脚本或外部脚本的形式包含在网页中。
通常,大多数浏览器时并行下载组件的,但对于外部脚本并非如此。当浏览器开始下载外部脚本时,在脚本下载,解析并执行完毕之前,不会开始下载任何其他内容(任何已经在进程中的下载都不会被阻塞)
浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或JavaScript的名字空间,它们会对后续内容造成影响。
结论:脚本必须按顺序执行,但没必要按顺序下载。
下面列出的几种技术即拥有外部脚本的好处,又能避免因阻塞导致的减速影响。
const scriptElem = document.createElement('script');
scriptElem.src = 'http://xxxxx/a.js';
document.getElementsByTagName('head')[0].appendChild(scriptElem);
浏览器忙指示器:
浏览器提供了多种忙指示器(状态栏,进度条,标签页图标和光标,还有阻塞渲染和阻塞onload),让用户感知到页面还在加载。
阻塞渲染:浏览器停止渲染所有脚本后面的内容,也就是用冻结页面来表示浏览器正忙。
阻塞onload:等到所有的资源下载完成时,才会触发onload事件。
理解每种技术如何对浏览器忙指示器产生影响相当重要。在某些情况下,为了更好的用户体验,我们需要忙指示器,让用户知道页面正在运行。其他情况下,最后时不显示忙指示器,从而鼓励用户开始与页面进行交互。
结论:
真正的最佳方案取决于需求。
我们需要考虑脚步的执行顺序是否与下载顺序有关系。如果其他资源与脚步并行下载且按顺序执行,那么需要根据不同的浏览器来综合使用多种技术。如果加载顺序无所谓,可以选用XHR Eval 或 XHR注入技术。另外,我们还要考虑各浏览器在忙指示器处理上的不同效果。
五:整合异步脚步
前面讨论了外部脚步的执行顺序,但大多数网页在加载外部脚本的同时也包含了使用了外部脚本定义标识符的行内脚本。
脚本如果按常规方式加载
当异步加载的外部脚本与行内脚本存在代码依赖时,我们必须通过一种保证执行顺序的方法来整合这两个脚本。
技术方案:
<script src="test.js" type="text/javascript">
function init() {
// ...
}
init();
</script>
该方法优点:
a. 更干净:只有一个script标签。
b. 更清晰:行内代码对外部脚本的依赖关系一目了然。
c. 更安全:如果外部脚本加载失败,行内代码不会执行,避免了未定义标识符的错误。
该方法缺点:
a. 浏览器不支持这种语法,需要在脚本最后添加处理代码(在DOM中搜素它本身的script元素,执行script的innerHTML)
b. 如果脚本不是异步加载的,我们需要把其中一种异步加载技术和该模式结合起来。(如,使用Script DOM Element无阻塞技术,行内代码通过动态设置script元素的text/innerHTML属性来执行函数)
多个外部脚本
前面研究的是整合单个外部脚本和行内脚本。这里将讨论异步加载多个外部脚本的技术,并同时保持外部脚本和行内脚本的执行顺序。
技术方案:
六:布置行内脚本
前面讨论的重心在外部脚本的影响上,接下来,我来看看行内脚本的影响。
行内脚本阻塞并行下载:
行内脚本除了阻塞并行下载,还会阻塞渲染。
解决方案:
保持CSS和JavaScript的执行顺序
CSS的应用规则同时适用于样式表和行内样式。浏览器会等待下载时间长的样式表下载完成,以保证CSS是按照指定的顺序应用的。即,在样式表后面的行内脚本会阻塞所有后续资源的下载。
因为行内脚本可能含有依赖于样式表中样式的代码。浏览器按顺序下载样式表和执行行内脚本是为了保证一致的结果。
解决方法:
调整行内脚本的位置,使其不出现在样式表和任何其他资源之间。即,行内脚本应该放在样式表之前或其他资源之后,如果其他资源是脚本,行内脚本与外部脚本之间可能会有代码依赖,应该放在样式表之前。如果确定没有代码依赖,那么可以移到可见资源之后。
七:编写高效的JavaScript
当执行JavaScript代码时,JavaScript引擎会创建一个执行上下文(也被称为作用域),它设定了代码执行时所处的环境。
JavaScript引擎会在页面加载后创建一个全局的执行上下文,然后每执行一个函数时都会创建一个对应的执行上下文。最终建立一个执行上下文的堆栈,当前起作用的执行上下文在堆栈的最顶层。
每个执行上下文都有一个与之关联的作用域链,用于解析标识符。作用域链包含一个或多个变量对象,这些对象定义来执行上下文作用域内的标识符。全局执行上下文的作用域链中只有一个变量,它定义了JavaScript中所有可用的全局变量和函数。当函数被创建(不是执行)时,JavaScript引擎会把创建时执行上下文的作用域链赋给函数的内部属性【[scope]】(内部属性不能通过JavaScript来存取,所以无法直接访问此属性)。然后,当函数被执行时,JavaScript引擎会创建一个活动对象,并在初始化时给this、arguments、命名参数和该函数的所有局部变量赋值。活动对象会出现在执行上下文作用域链的顶端,紧接其后的是函数【[Scope]】属性中的对象。
请记住,全局变量对象始终是作用域链中的最后一个对象,所以对全局标识符的解析总是最耗时的。应该尽可能的使用局部变量,因为它们存在于执行函数的活动对象中,解析标识符只需要查找作用域链中的单个对象。读取变量值的总耗时随着查找作用域链的逐层深入而不断增长,所以标识符越深,存取速度越慢。
增长作用域链
with语句,用于将对象属性作为局部变量来显示,使其便于访问。
with语句在反复使用同一对象属性时看起来很方便,但在作用域链中增加的额外对象影响了对局部标识符的解析。当执行with语句中的代码时,函数中的局部变量将从作用域链的第一个对象变为第二个对象,自然而然会减慢标识符的存取。with语句执行结束,作用域链将恢复到原来的状态。(因为这个主要缺陷,建议避免使用它)
try-catch语句块中的catch从句。
在执行catch从句中的代码时,其行为方式类似于with语句,也是在作用域链的顶部增加了一个对象。该对象包含了由catch指定命名的异常对象。由于catch从句仅在执行try从句发生错误时才执行,所以它比with语句的影响要小。
高效的数据存取
数据在脚本中存储的位置直接影响脚本执行的总耗时。一般而言,在脚本中有4种地方可以存取数据:
在大多数浏览器中,从字面量中读取值和从局部变量中读取值的开销差异很小,以至于可以忽略不计。真正的差异在于从数组或对象中读取数据。
在数据存取时,将函数中使用超过一次的对象属性或数组元素存储为局部变量是一种好方法。
快速条件判断
总结:
快速循环
JavaScript中有4种不同类型的循环:for循环,do-while循环,while循环和for-in循环。
循环性能的简单提升:
注意:小心使用数组原生的indexOf方法,这个方法遍历数组成员的耗时可能会比使用普通的循环还长。
避免for-in循环:
由于for-in循环用途特殊,所以很少有什么地方能改善其性能。它的结束条件无法改变,而且遍历属性的顺序也无法改变。它通常比其他循环慢,因为它需要从一个特定的对象中解析每个可枚举的属性,它为了提取这些属性需要检查对象的原型和整个原型链,遍历原型链就像遍历作用域链,会增加耗时,从而降低整个循环的性能。
展开循环:
var i = values.length;
while(i--){
process(values[i]);
}
// 展开循环
process(values[0]);
process(values[1]);
process(values[2]);
process(values[3]);
process(values[4]);
通过限制循环的次数来减少循环的开销。
这种方法虽然降低了维护性,但它需要编写更多的代码。此外,为了从这样少量的语句中获得性能提升而提高维护成本是不值得的。
然而当你处理大量的值且循环次数可能很多时,这项技术就非常有用了。这种模式称为Duff策略。
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;
do {
switch(startAt){
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt = 0;
} while (--iterations > 0);
Duff策略背后的思想是每一次循环完成标准循环的1~8次。当需要进行大量循环时,使用Duff策略比标准循环要快的多,但其实它还可以更快些。
另一个版本,把对额外数组项的处理移到主循环外,这样就可以去掉switch语句,从而得到一个处理大数组的更快方法:
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if(leftover > 0){
do{
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while(--iterations > 0);
字符串优化
字符串连接
有通过加法运算符(+)来完成的 和 通过Array对象的join方法来连接的。
第一版:
该实现方式有个基于正则表达式的性能问题。一方面是指明有两个匹配模式的管道运算符,另一方面是指明全局应用该模式的g标记。
function trim(text) {
return text.replace(/^\s+|\s+$/g, "");
}
第二版:
考虑到第一版的缺陷,可以将正则表达式一分为二并去掉g标记来重写该函数,以此来稍稍提高它的速度。
function trim(text) {
return text.replace(/^\s+/, "").replace(/\s+$/, "");
}
第三版:
function trim(text) {
// 删除头部空白
text = text.replace(/^\s+/, "");
// 通过for循环清除尾部的空白
for(var i = text.length - 1; i >= 0; i--) {
if(/\S/.test(text.charAt(i))) {
text = text.substring(0, i + 1);
break;
}
}
return text;
}
避免运行时间过长的脚本
总结:
八:可伸缩的Comet
Comet:基于HTTP长连接的“服务器推”技术
Comet的目标包括随时从服务端向客户端推送数据,提升传统Ajax的速度和可扩展性,以及开发事件驱动的Web应用。
传统web请求,是显式的向服务器发送http Request,拿到Response后显示在浏览器页面上。这种被动的交互方式不能满足对信息实时性要求高的应用,譬如聊天室、股票交易行情、在线游戏等。Ajax轮询虽然可以解决这个问题,但是会带来增加服务器负担、带宽浪费,并且这种实现方式不够优雅。而Comet技术就是为此而生的。
传输技术:
十:图像优化
真彩色图形:
使用RGB颜色模型可以展示1600多万种颜色(256256256或2^24)
调色板图像格式
将图像中各种不同颜色提取出来建立一个表,这个表叫调色板(也可以称为索引)。通过将调色板中的条目和每个像素重新匹配,就可以达到重新绘制整个图像的目的。
不同图像格式的特性:
GIF:
JPEG:
PNG:
高度优化的CSS Sprite:
十一:划分主域
十二:尽早刷新文档的输出
十三:少用iframe
iframe开销很高
十四:简化CSS选择符
编写高效的CSS选择符: