《高性能网站建设进阶指南》阅读笔记

一: Web性能提升14条规则:

  1. 尽量减少HTTP请求。
  2. 使用CDN。
  3. 添加Expires头,设置过期时间。 https://www.sohu.com/a/306012294_744669
  4. 采用Gzip压缩组件。
  5. 将样式表放在顶部。
  6. 将脚本放在底部。
  7. 避免css表达式。
  8. 使用外部的JavaScript和CSS。
  9. 减少DNS查询。
  10. 精简JavaScript。
  11. 避免重定向。
  12. 删除重复的脚本。
  13. 配置ETag。
  14. 使Ajax可缓存。

在做性能优化时,不要浪费时间去尝试为那些不消耗大量时间的代码提速。
评估优先,拒绝任何不能提供良好效益的优化。

高性能网站“足够快”的评估准则:

  1. 超过0.1秒:给人不够平滑快捷的感觉。
  2. 超过1秒:感到应用程序缓慢。(要提示用户,计算机正在解决这个问题,例如改变光标形状)
  3. 超过10秒:用户将非常沮丧。(需要有百分比完成指示器,以及方便用户中断操作且有清晰标识的方法)

二:创建快速响应的Web应用

  1. 创建快速响应网页的一个关键方面是:复杂计算

别把运行时间可能会很长的低性能代码引入到网页中。

然而, 有时执行任务的开销非常高,且无法神奇地把它优化得耗时更少。这种导致用户界面出现糟糕停滞的情形无法避免吗?就没有一个能让用户顺利执行的解决方案吗?
在这种情况下,传统的解决方案是使用多线程来把开销很大的代码从与用户交互的线程中剥离开来。然而,JavaScript并不支持多线程。

JS引擎是单线程的: https://blog.csdn.net/HuangsTing/article/details/111830927

因为,多线程在各个方面违反了抽象概念,主要是产生了竞争状态,死锁的风险和悲观锁定开销,并且它们无法横向扩展去处理未来超级内核的亿万次计算能力。
简单来说,就是不同的线程可以访问并修改相同的变量。但出现A线程要修改B线程正在修改的变量或类似情况时,这会导致各种各样的问题。

我们需要的是一种像多线程那样能多任务并发执行,却没有线程之间相互侵入危险的方法。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。(所以,这个新标准并没有改变 JavaScript 单线程的本质)

在页面上任何开销可能很大的,(例如,长时间运行的)JavaScript操作都应该委托给Worker。

  1. 创建快速响应网页的另一个关键方面是:内存管理

主要有以下2点:

  1. 垃圾回收导致的停顿(离散且孤立的停顿,间歇式的发生并且停顿的长度会随时间而增长)
    自动内存管理是有开销的。在执行垃圾回收(简称:“GC”)时,GC实现中最复杂的几乎是“stop the world”,它会冻结整个运行环境(包括正在调用的主浏览器JavaScript线程),直到遍历完整个创建对象的“堆”。在这个过程中,它会查找那些不再使用或能够回收未用内存的对象。
    但随着应用程序内存占用的增加,这个过程所需的时间将增长并最终达到引起用户注意的程度。
  2. 内存分页导致的停顿(全面的,无处不在的停顿)
    操作系统为应用程序提供可用的内存:物理内存和虚拟内存。
    物理内存映射在极快的RAM芯片上;虚拟内存映射在非常慢的海量存储设备上(比如硬盘);
    但网页中内存需求增长到足够大,就可能会迫使操作系统开始内存分页(一个极慢的进程凭借迫使其他进程放弃真正的内存来给浏览器不断增长的需求腾出空间)
    当发生内存分页时,系统把内存页从物理内存转移到虚拟内存(例如,从RAM到硬盘上)反之亦然。

优化内存的方法:

三:拆分初始化负载

web应用程序很大一部分的应用代码不会在启动时被使用。
为了能尽可能快速渲染出网页,可以把JavaScript代码分为两部分:

  1. 渲染初始页面的必须代码。(还有一些虽未被使用,但仍然必需的:如,错误处理和一些条件判断代码)
  2. 除了符合第一点条件,剩下的代码。

注意:拆分JavaScript代码要避免出现未定义标识符错误。(在JavaScript执行时引用到一个被降级到延迟加载的标志符时,就会出现这种问题)

解决方式:

  1. 在延迟加载的代码与用户界面元素相关联的情况下:

● 可以通过改变元素的展现来解决此问题。如,显示“加载中…”的图标。
● 在延迟加载的代码里绑定界面元素的事件处理程序。(例如,页面中的下拉菜单被初始化为一个静态文本,点击它不会执行任何JavaScript。延迟加载的代码包括菜单的功能和事件的绑定,通过attacEvent或addEventListener来实现)

  1. 在延迟加载的代码不与界面元素相关联的情况下:

使用桩(stub)函数来解决。

● 桩函数是一个与原函数名称相同但是函数体为空,或是用一些临时代码代替原有内容的函数。在初始化下载的代码中插入桩函数,当调用它们时,动态加载其他的JavaScript代码。当新增的JavaScript代码下载完成,原函数会覆盖桩函数。
● 可以给每一个被引用但又被降级为延迟下载的函数创建一个桩函数。桩函数可以返回一个桩值,例如一个字符串。稍微高级一点的使用,可以用桩函数记录用户的请求,并在JavaScript完成加载时调用相应的代码。

除了拆分JavaScript,拆分CSS样式表也是有益的。不过相对于拆分JavaScript,后者节省的资源要少一些,并且样式表整体大小通常比JavaScript小,且下载CSS并不会像JavaScript那样具有阻塞特性。

四:无阻塞加载脚本

script标签的阻塞行为会对页面性能产生负面的影响。但有时候,这种阻塞是必要的。因此,能够识别出JavaScript不依赖于页面中其他内容而单独加载的情况,是非常重要的。

JavaScript以行内脚本或外部脚本的形式包含在网页中。
通常,大多数浏览器时并行下载组件的,但对于外部脚本并非如此。当浏览器开始下载外部脚本时,在脚本下载,解析并执行完毕之前,不会开始下载任何其他内容(任何已经在进程中的下载都不会被阻塞)

浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或JavaScript的名字空间,它们会对后续内容造成影响。

结论:脚本必须按顺序执行,但没必要按顺序下载。

下面列出的几种技术即拥有外部脚本的好处,又能避免因阻塞导致的减速影响。

  1. XHR Eval
    通过XMLHttpRequest(XHR)从服务器获取脚本后,再通过eval命令执行内容。
  2. XHR注入(XHR Injection)
    类似于XHR Eval。XHR注入技术也是通过XMLHttpRequest获取脚本内容。然后,创建一个script 的DOM元素,把XMLHttpRequest的响应注入script中来执行JavaScript。
  3. Script in Iframe
    因为主页中iframe和其他组件时并行加载的。利用iframe无阻塞加载JavaScript: 引入文档a.html,在文档中写入脚步。
    注意:实际上iframe是开销最高的DOM元素。
  4. Script DOM Element
    使用JavaScript动态创建script DOM元素并设置其src属性。
const scriptElem = document.createElement('script');
scriptElem.src = 'http://xxxxx/a.js';
document.getElementsByTagName('head')[0].appendChild(scriptElem);
  1. Script Defer
    设置script标签属性
  2. document.write Script Tag
    使用document.write把script标签写入页面。

浏览器忙指示器:

浏览器提供了多种忙指示器(状态栏,进度条,标签页图标和光标,还有阻塞渲染和阻塞onload),让用户感知到页面还在加载。

阻塞渲染:浏览器停止渲染所有脚本后面的内容,也就是用冻结页面来表示浏览器正忙。
阻塞onload:等到所有的资源下载完成时,才会触发onload事件。

理解每种技术如何对浏览器忙指示器产生影响相当重要。在某些情况下,为了更好的用户体验,我们需要忙指示器,让用户知道页面正在运行。其他情况下,最后时不显示忙指示器,从而鼓励用户开始与页面进行交互。

结论:

真正的最佳方案取决于需求。
我们需要考虑脚步的执行顺序是否与下载顺序有关系。如果其他资源与脚步并行下载且按顺序执行,那么需要根据不同的浏览器来综合使用多种技术。如果加载顺序无所谓,可以选用XHR Eval 或 XHR注入技术。另外,我们还要考虑各浏览器在忙指示器处理上的不同效果。

五:整合异步脚步

前面讨论了外部脚步的执行顺序,但大多数网页在加载外部脚本的同时也包含了使用了外部脚本定义标识符的行内脚本。

脚本如果按常规方式加载

当异步加载的外部脚本与行内脚本存在代码依赖时,我们必须通过一种保证执行顺序的方法来整合这两个脚本。

技术方案:

  1. 硬编码回调
    让外部脚本调用行内代码里的函数。但是我们往往不太可能把回调嵌入第三方JavaScript模块中,这种方法也不灵活。
  2. Window Onload
    监听window的onload事件来触发行内代码的执行。当着需要确保外部脚本在window.onload之前下载执行。
    以下的异步加载技术能确保这点:
  3. Script in Iframe
  4. Script DOM Element
  5. Script Defer
    该方法缺点:
  6. 必须确保异步加载脚本是通过阻塞onload事件的方式加载的。
  7. 可能会导致行内代码的延迟执行。
  8. 如果页面还有更多的资源(图片等),那么需要等这些资源加载完,才会触发onload事件。
  9. 定时器
    通过定时器轮询,来保证在行内代码执行之前所依赖的外部脚本已经加载。
  10. Script Onload
    Script Onload 技术是整合异步加载外部脚本和行内脚本的首选。
    前面的整合技术会增加页面的脆弱性,延迟和开销。而Script Onload是通过script元素上的onload和onreadystatchange事件处理程序,监听脚本的onload事件。
  11. 降级使用script标签
    该方法使用script标签来包含外部脚本和使用它的行内代码,如下:
<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属性来执行函数)

多个外部脚本

前面研究的是整合单个外部脚本和行内脚本。这里将讨论异步加载多个外部脚本的技术,并同时保持外部脚本和行内脚本的执行顺序。

技术方案:

  1. Managed XHR
    XHR注入技术在所有浏览器中都不能保持执行顺序,该方法把XHR响应加入队列来保证它们按顺序执行这部分的内容。
    这种技术非常适用于按特定顺序加载外部脚本和行内脚本的使用场景,而且不阻塞页面中其他资源。但是它只能用于和主页面同域的脚本。
  2. DOM Element 和 Doc Write

六:布置行内脚本

前面讨论的重心在外部脚本的影响上,接下来,我来看看行内脚本的影响。

行内脚本阻塞并行下载:
行内脚本除了阻塞并行下载,还会阻塞渲染。

解决方案:

  1. 把行内脚本移至底部。
    这种方法可以避免阻塞下载,但它依旧阻塞渲染。
  2. 使用异步回调启动JavaScript的执行。
    如果行内脚本执行时间很短,使用延迟值为0毫秒的setTimeout是一个兼顾快速渲染和JavaScript快速执行的好方法。如果脚本执行时间很长,更好的选择是使用onload。
  3. 使用script的defer属性。

保持CSS和JavaScript的执行顺序
CSS的应用规则同时适用于样式表和行内样式。浏览器会等待下载时间长的样式表下载完成,以保证CSS是按照指定的顺序应用的。即,在样式表后面的行内脚本会阻塞所有后续资源的下载。
因为行内脚本可能含有依赖于样式表中样式的代码。浏览器按顺序下载样式表和执行行内脚本是为了保证一致的结果。
解决方法:
调整行内脚本的位置,使其不出现在样式表和任何其他资源之间。即,行内脚本应该放在样式表之前或其他资源之后,如果其他资源是脚本,行内脚本与外部脚本之间可能会有代码依赖,应该放在样式表之前。如果确定没有代码依赖,那么可以移到可见资源之后。

七:编写高效的JavaScript

  1. 管理作用域

当执行JavaScript代码时,JavaScript引擎会创建一个执行上下文(也被称为作用域),它设定了代码执行时所处的环境。
JavaScript引擎会在页面加载后创建一个全局的执行上下文,然后每执行一个函数时都会创建一个对应的执行上下文。最终建立一个执行上下文的堆栈,当前起作用的执行上下文在堆栈的最顶层。
每个执行上下文都有一个与之关联的作用域链,用于解析标识符。作用域链包含一个或多个变量对象,这些对象定义来执行上下文作用域内的标识符。全局执行上下文的作用域链中只有一个变量,它定义了JavaScript中所有可用的全局变量和函数。当函数被创建(不是执行)时,JavaScript引擎会把创建时执行上下文的作用域链赋给函数的内部属性【[scope]】(内部属性不能通过JavaScript来存取,所以无法直接访问此属性)。然后,当函数被执行时,JavaScript引擎会创建一个活动对象,并在初始化时给this、arguments、命名参数和该函数的所有局部变量赋值。活动对象会出现在执行上下文作用域链的顶端,紧接其后的是函数【[Scope]】属性中的对象。

  1. 使用局部变量

请记住,全局变量对象始终是作用域链中的最后一个对象,所以对全局标识符的解析总是最耗时的。应该尽可能的使用局部变量,因为它们存在于执行函数的活动对象中,解析标识符只需要查找作用域链中的单个对象。读取变量值的总耗时随着查找作用域链的逐层深入而不断增长,所以标识符越深,存取速度越慢。

  1. 增长作用域链

  2. with语句,用于将对象属性作为局部变量来显示,使其便于访问。
    with语句在反复使用同一对象属性时看起来很方便,但在作用域链中增加的额外对象影响了对局部标识符的解析。当执行with语句中的代码时,函数中的局部变量将从作用域链的第一个对象变为第二个对象,自然而然会减慢标识符的存取。with语句执行结束,作用域链将恢复到原来的状态。(因为这个主要缺陷,建议避免使用它)

  3. try-catch语句块中的catch从句。
    在执行catch从句中的代码时,其行为方式类似于with语句,也是在作用域链的顶部增加了一个对象。该对象包含了由catch指定命名的异常对象。由于catch从句仅在执行try从句发生错误时才执行,所以它比with语句的影响要小。

  4. 高效的数据存取

数据在脚本中存储的位置直接影响脚本执行的总耗时。一般而言,在脚本中有4种地方可以存取数据:

  1. 字面量值
  2. 变量
  3. 数组元素
  4. 对象属性

在大多数浏览器中,从字面量中读取值和从局部变量中读取值的开销差异很小,以至于可以忽略不计。真正的差异在于从数组或对象中读取数据。
在数据存取时,将函数中使用超过一次的对象属性或数组元素存储为局部变量是一种好方法。

  1. 流控制

快速条件判断

  1. 在JavaScript中,当仅判断一两个条件时,if语句通常比switch语句更快。当有两个以上条件且条件比较简单(不是进行范围判断)时,switch语句往往更快。因为大多数情况下,switch语句中执行单个条件所需时间比在if语句中短,所以当有大量的条件判断时,使用switch语句更合适。(可以通过按出现频率的降序排列条件来提高switch语句的性能)
  2. 数据查询
    把所有的结果都存储在数组中,并用数组的索引映射value变量。检索对应的结果就是简单的查询数组值。

总结:

  1. 使用if语句:两个之内的离散值需要判断;大量的值能容易的分到不同的区间范围中。
  2. 使用switch语句:超过两个而少于10个离散值需要判断;条件值是非线性的,无法分离出区间范围。
  3. 使用数组查询:超过10个值需要判断;条件对应的结果是单一值,而不是一系列操作。

快速循环

JavaScript中有4种不同类型的循环:for循环,do-while循环,while循环和for-in循环。

循环性能的简单提升:

  1. 使用局部变量来代替属性查找能加快循环的执行效率。
  2. 将循环变量递减到0,而不是递增到总长度。

注意:小心使用数组原生的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);
  1. 字符串优化

  2. 字符串连接

有通过加法运算符(+)来完成的 和 通过Array对象的join方法来连接的。

  1. 裁剪字符串

第一版:
该实现方式有个基于正则表达式的性能问题。一方面是指明有两个匹配模式的管道运算符,另一方面是指明全局应用该模式的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;
}

避免运行时间过长的脚本
总结:

  1. 管理作用域,存取非局部变量要比局部变量耗时更多。
  2. 存储和读取数据的方式对脚本性能影响极大。
  3. 流控制也是影响脚本执行速度的一个重要因素。
  4. 在JavaScript中,循环经常会成为性能瓶颈。为了使循环最高效,可以采用倒序的方式来处理元素。(在控制条件中,将迭代变量和0作比较)
  5. 谨慎使用HTMLCollection对象。每次存取这类对象的时候,都会重新查询DOM中匹配的节点。
  6. 常见的字符串操作可能会带来意料之外的性能问题。
  7. 浏览器会限制JavaScript可以运行的最长时间。

八:可伸缩的Comet
Comet:基于HTTP长连接的“服务器推”技术
Comet的目标包括随时从服务端向客户端推送数据,提升传统Ajax的速度和可扩展性,以及开发事件驱动的Web应用。
传统web请求,是显式的向服务器发送http Request,拿到Response后显示在浏览器页面上。这种被动的交互方式不能满足对信息实时性要求高的应用,譬如聊天室、股票交易行情、在线游戏等。Ajax轮询虽然可以解决这个问题,但是会带来增加服务器负担、带宽浪费,并且这种实现方式不够优雅。而Comet技术就是为此而生的。

传输技术:

  1. 轮询(简单轮询,每x毫秒发出一个请求来检查是否有更新需要呈现到用户界面上)
  2. 长轮询
  3. 永久帧
  4. XHR流
    九:超越Gzip压缩

十:图像优化
真彩色图形:
使用RGB颜色模型可以展示1600多万种颜色(256256256或2^24)
调色板图像格式
将图像中各种不同颜色提取出来建立一个表,这个表叫调色板(也可以称为索引)。通过将调色板中的条目和每个像素重新匹配,就可以达到重新绘制整个图像的目的。

不同图像格式的特性:
GIF:

  1. 透明。允许一个二进制(是/否)类型的透明度。但它不支持alpha(可变的)透明。
  2. 动画
  3. 无损:修改保存关闭时,不会损失任何质量。
  4. 逐行扫描
  5. 隔行扫描

JPEG:

  1. 有损。
  2. 不支持透明和动画。
  3. 隔行扫描。

PNG:

  1. 可分为:调色板PNG格式和真彩色PNG格式。
  2. 支持完全的alpha透明。
  3. png动画
  4. 无损
  5. 逐行扫描
  6. 隔行扫描
  7. PNG8:调色板PNG的别称。PNG24:真彩色PNG的别称,不包括alpha通道。PNG32:真彩色PNG的别称,包括alpha通道。

高度优化的CSS Sprite:

  1. 按照颜色合并。
  2. 避免不必要的空白。
  3. 将元素水平排列。
  4. 将颜色限制在256种以内。
  5. 先优化单独的图像,再优化Sprite。
  6. 通过控制大小和对齐减少反锯齿像素的数量。
  7. 避免使用对角线渐变。
  8. 每2~3个像素改变渐变的颜色, 而不是每个像素都改变。
  9. 处理Logo的时候要小心,非常小的修改也会很容易被注意到。

十一:划分主域
十二:尽早刷新文档的输出
十三:少用iframe
iframe开销很高
十四:简化CSS选择符
编写高效的CSS选择符:

  1. 避免使用通配规则
  2. 不要限定ID选择符
  3. 不要限定类选择符
  4. 让规则越具体越好
  5. 避免使用后代选择符
  6. 避免使用标签-子选择符
  7. 质疑子选择符的所有用途,尽可能用具体的类取代它
  8. 依靠继承,避免对属性重复指定规则
    十五:性能工具
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神小夜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值