思维导图链接:http://v3.processon.com/view/link/5f7ec46b762131119546c87d
取材自《高性能网站建设指南》及《高性能网站建设进阶指南》
本文只是个人读书笔记,更多详细内容请查看原书。
高性能网站建设指南
- 1. 减少HTTP请求
- CSS Sprites:干净的标签,很少的图片和很短的响应时间
- Inline Images:data:[<mediatype>][;base64],<data>
- 由于data:URL是内联在页面中的,在跨越不同页面时不会被缓存。
- 使用CSS并将内将内联图片作为背景,将该CSS规则放在外部样式表中,这意味着数据可以缓存在样式表内部。
- 合并脚本和样式表
- 2. 使用内容发布网络CDN
- 如果应用程序Web服务器(Application Web Server)离用户更近,则一个HTTP请求的响应时间将缩短。如果组件Web服务器(Component Web Server)离用户更近,则多个HTTP请求的响应时间将缩短。
- CND是一组分布在多个不同地理位置的Web服务器,用于更加有效地向用户发布内容。
- CDN有助于缓和Web流量峰值压力。
- 依赖CDN的一个缺点是你的响应时间可能会受到其他网站——甚至很可能是你的竞争对手流量的影响。CDN服务提供商在其所有客户之间共享其Web服务器组。另一个缺点是无法直接控制组件服务器所带来的特殊麻烦。
- CDN用于发布静态内容,如图片、脚本、样式表和Flash
- 如果你以你自己的响应时间测试衡量使用CDN的优势,千万记住你运行测试的地理位置对结果有着重要影响。
- 3. 添加Expires头
- 浏览器(和代理)使用缓存来减少HTTP请求的数量,并减小HTTP响应的大小,使Web页面加载得更快。Web服务器使用Expires头来告诉Web客户端它可以使用一个组件的当前副本,直到指定的时间为止。
- 因为Expires头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外,过期日期需要经常检查,并且一旦未来这一天到来了,还需要在服务器配置中提供一个新的日期。
- 使用带有max-age的Cache-Control可以消除Expires的限制(HTTP1.1)
- mod_expires Apache模块使你在使用Expires头时能像max-age那样以相对的方式设置日期。这通过Expires-Default指令来完成,它同时向响应中发送Expires头和Cache-Control max-age头,实际的过期日期根据何时接到请求而变。由于Cache-Control具有优先权并且明确指出了相对于请求时间所经过的秒数,时钟同步问题就被避免了。
- 跨浏览器改善缓存的最佳解决方案就是使用由ExpiresDefault设置的Expires头。
- 4. 压缩组件(脚本和样式表)
- Accept-Encoding:gzip,deflate(HTTP1.1)
- 根据经验通常对大于1KB或2KB的文件进行压缩。mod_gzip_minimum_file_size指令控制着希望压缩的文件的最小值,默认值是500B。
- 当浏览器直接与服务器通信时,Web服务器基于Accept-Encoding来检测是否对响应进行压缩。不管是否压缩过,浏览器都会基于响应中的其他HTTP头如Expires和Cache-Control来缓存响应。
- 当浏览器通过代理来发送请求时:假设针对某个URL发送到代理的第一个请求来自于一个不支持gzip的浏览器。这是到达代理的第一个请求,因此其缓存为空。代理会将请求转发到Web服务器。此时服务器的响应是未经过压缩的。这个没有压缩的响应被代理缓存起来并发送给浏览器。现在,假设到达代理的第二个请求访问的是同一个URL,来自于一个支持gzip的浏览器。代理会使用其缓存中(未经压缩)的内容进行响应,这就失去了进行压缩的机会。如果顺序反了——第一个请求来自于一个支持gzip的浏览器,而第二个请求来自于一个不支持gzip的浏览器——情况可能更严重。在这种情况下,代理的缓存中拥有内容的一个压缩版本,并将这个版本提供给后续的浏览器,而不管它们是否支持gzip。
- 解决上述问题的方法是在Web服务器的响应中添加Vary头——Vary:Accept-Encoding。Web服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。这将使得代理缓存响应的多个版本,为Accept-Encoding请求头的每个值缓存一份。
- 前面的例子中,代理会缓存每个响应的两个版本——Accept-Encoding为gzip时的压缩内容和没有指定Accept-Encoding时的非压缩内容。当浏览器带着Accept-Encoding:gzip访问代理时,它接收到的是压缩过的内容。没有Accept-Encoding请求头的浏览器收到的是未经压缩的内容。
- 默认情况下,mod_gzip会向所有响应添加Vary:Accept-Encoding头,以驱使代理执行正确的操作。
- 边缘情形:无论是客户端还是服务器端发送错误(发送压缩内容到不支持它的客户端、忘记将压缩内容声明为已经进行了gzip编码等),页面都会被破坏。
- 在Apache的mod_gzip中,可以通过在mod_gzip_item_include中使用恰当的User-Agent值来指定浏览器白名单:mod_gzip_item_include reqheader "User-Agent: MSIE[6-9]"mod_gzip_item_include reqheader "User-Agent: Mozilla/[5-9]"
- 由于不能和代理共享浏览器白名单配置,所以需要将User-Agent作为代理的另外一种评判标准添加到Vary头中:Vary: Accept-Encoding,User-Agent当mod_gzip检测到你在使用浏览器白名单时,它会自动将User-Agent字段添加到Vary头。
- 由于使用对User-Agent HTTP头进行求值的过滤器规则将会导致完全禁用为响应包进行的缓存,破坏了代理缓存,所以可以使用Vary:*(防止浏览器使用缓存的组件)或Cache-Control:private头(推荐)来禁用代理缓存。这种方式是为所有浏览器禁用代理缓存,因此会增加你的带宽开销。
- 如何平衡压缩和代理支持的决定是很复杂的,需要在加快响应时间、减小带宽开销和边缘情形浏览器缺陷之间进行权衡。
- 如果你的网站用户很少,并且他们处在一个小圈子中,边缘情形浏览器就不需要太多关注。可以压缩内容并使用Vary:Accept-Encoding。这样可以通过减小组件的大小和利用代理缓存来改善用户体验。
- 如果你要注意带宽开销,可以和前一种情况一样——压缩内容并使用Vary:Accept-Encoding。这降低了服务器端的带宽开销并提升了代理处理的请求数量。
- 如果你拥有大量的、多变的用户群,能够应付较高的带宽开销,并且享有高质量的名声,请压缩内容并使用Cache-Control:Private。这禁用了代理但避免了边缘情形缺陷。
- 5. 将样式表放在顶部
- 将样式表放在文档底部,为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容逐步呈现。
- 将样式表放在顶部对于加载页面所需的实际时间没有太多影响,它影响更多的是浏览器对这些组件顺序的反应。实际上,用户感觉缓慢的页面反而是可视化组建加载得更快的页面。
- 在IE中,将样式表放在文档底部会导致白屏问题的情形有以下几种
- 1. 在新窗口中打开时
- 2. 重新加载时
- 3. 作为主页
- 白屏是对无样式内容的闪烁FOUC问题的弥补。浏览器可以延迟呈现,直到所有的样式表都下载完之后,这就导致了白屏。反之,浏览器可以逐步呈现,但要承担闪烁的风险。
- 使用@import规则会导致组件下载时的无序性。
- 使用Link标签将样式表放在文档的Head中
- 6. 将脚本放在底部
- 并行下载
- 对响应时间影响最大的是页面中组件的数量
- 过多的并行下载反而会降低性能
- 在下载脚本时并行下载实际上是被禁用的——即使使用了不同的主机名,浏览器也不会启动其他的下载。其中一个原因是,脚本可能使用document.write来修改页面内容,因此浏览器会等待,以确保页面能够恰当地布局。另一个原因是为了保证脚本能够按照正确的顺序执行。如果并行下载多个脚本,就无法保证响应是按照特定顺序达到浏览器的。
- 脚本会阻塞对其后面内容的呈现,会阻塞对其后面组件的下载。
- 建议使用延迟Defferred脚本。DEFER属性表明脚本不包含document.write,浏览器得到这一线索就可继续进行呈现。
- Script Onload技术是整合异步加载脚本和行内脚本的首选。
- 在样式表后面的行内脚本会阻塞所有后续资源的下载。解决方案:调整行内脚本的位置,使其不出现在样式表和任何其他资源之间。
- 并行下载
- 7. 避免CSS表达式
- 表达式的问题在于对其进行的求值的频率比人们期望的要高。它们不只在页面呈现和大小改变时求值,当页面滚动、甚至用户鼠标在页面上移过时都要求值。
- 创建一次性表达式和使用事件处理器取代CSS表达式
- 8. 使用外部JavaScript和CSS
- 9. 减少DNS查找
- 很多浏览器拥有其自己的缓存,和操作系统的缓存相分离。只要浏览器在其缓存中保留了DNS记录,它就不会麻烦OS来请求这个记录。只有当浏览器缓存丢弃了记录时,它才会向操作系统询问地址——然后OS或通过其缓存来响应这个请求,或将请求发送给一台远程服务器,这时就会发生潜在的速度降低。
- 浏览器对缓存的DNS记录的数量也有限制,而不管缓存记录的时间。
- 当客户端的DNS缓存为空(浏览器和OS都是)时,DNS查找的数量与Web页面中唯一主机名的数量相等。减少唯一主机名的数量就可以减少DNS查找的数量,但会潜在地减少页面中并行下载的数量。避免DNS查找降低了响应时间,但减少并行下载可能会增加响应时间。建议将组建分别放到至少2个,但不超过4个主机名下。这是在减少DNS查找和允许高度并行下载间作出的很好的权衡。
- 通过使用Keep-Alive和较少的域名来减少DNS查找。
- 10. 精简JavaScript
- 精简(Minification)是从代码中移除不必要的字符以减小其大小,进而改善加载时间的实践。
- 混淆(Obfuscation)会移除注释和空白,还好改写代码,将函数名和变量名转换为更短的字符串,增加了反向工程的难度。
- 混淆JS有三个主要的缺点
- 缺陷:由于混淆更加复杂,混淆过程本身很有可能引入错误。
- 维护:由于混淆会改变JS符号,因此需要对任何不能改变的符号(例如API函数)进行标记,防止混淆器修改它们。
- 调试:经过混淆的代码很难阅读。这使得在产品环境中调试问题更新困难。
- 11. 避免重定向
- 重定向会延迟整个HTML文档的传输。
- 重定向的情形
- 1. 缺少结尾的斜线
- 2. 将旧网站连接到新网站
- 3. 跟踪内部流量
- 4. 跟踪出站流量
- 5. 美化URL
- 12. 移除重复脚本
- 重复脚本损伤性能的方式有两种——不必要的HTTP请求和执行JavaScript所浪费的时间。
- 不必要的HTTP请求会发生在IE中,而不会发生在Firefox中。在IE中,如果脚本被包含两次并且没有被缓存,浏览器会在页面加载期间产生两个HTTP请求。
- 13. 配置ETag
- 实体标签(Entity Tag,ETag)是Web服务器和浏览器用于确认缓存组件的有效性的一种机制。ETag(HTTP1.1)是唯一标识了一个组件的恶一个特定版本的字符串。唯一的格式约束是该字符串必须用引号引起来。
- 如果缓存的组件过期了(或用户明确地重新加载了页面),浏览器在重用它之前必须首先检查它是否仍然有效。这称作一个条件GET请求。
- 服务器在检测缓存的组件是否和原始服务器上的组件匹配时有两种方式
- 比较最新修改日期Last-Modified
- 浏览器会使用If-Modified-Since头将最新修改日期传回给原始服务器以进行比较。如果原始服务器上组件的最新修改日期与浏览器传回的值匹配,会返回一个304响应。
- 比较实体标签ETag
- ETag的加入为验证实体提供了比最新修改日期更为灵活的机制。如实体依据User-Agent或Accept-Language头而改变,实体的状态可以反映在ETag中。
- 如果浏览器必须验证一个组件,它会使用If-None_Match头将ETag传回原始服务器。如果ETag是匹配的,就会返回304状态码。
- 比较最新修改日期Last-Modified
- ETag的问题在于,通过使用组件的某些属性来构造它,这些属性对于特定的、寄宿了网站的服务器来说是唯一的。当浏览器从一台服务器上获取了原始组件,之后,又向另外一台不同的服务器发起条件GET请求时,ETag是不会匹配的——而对于使用服务器集群来处理请求的网站来说,这是很常见的一种情况。默认情况下,对于拥有多台服务器的网站,Apache和IIS向ETag中嵌入的数据都会大大地降低有效性验证的成功率。
- 如果ETag不匹配,则不会接收到更小更快的304响应,相反,会收到普通的200响应以及组件的所有数据。如果你使用服务器集群,则组件的下载次数可能会比必须进行下载的次数多得多,这将导致性能的下降。
- 对组件进行不必要的重新加载还会影响服务器的性能并增加带宽开销。
- ETag还降低了代理缓存的效率。代理后面的用户缓存的ETag经常和代理缓存的ETag不匹配,这导致了不必要的请求被发送到原始服务器。用户和代理之间不会出现304响应,而是会产生两个(又慢又大)的200响应——一个是从原始服务器到代理的,另一个是从代理到用户的。ETag的默认格式还可能会引入安全性弱点。
- 如果你的组件必须通过最新修改日期之外的一些东西来进行验证,则ETag是一种强大的方法。如果你无须自定义ETag,最好简单地将其移除。
- 可以配置ETag只包含大小和时间戳(Apache)或只有时间戳(IIS)。然而,因为这些都是重复信息,所以最好将ETag完全移除。
- 默认情况下,ETag不能反映出内容是否被压缩,因此代理可能会向浏览器提供错误的内容。最好的解决办法是禁用ETag。
- 14. 使Ajax可缓存
- 进阶
- “足够快”的界定
- 0.1秒:用户直接操作UI中对象的感觉极限。如从用户选择表格中的一列到该列高亮或向用户反馈已被选择的时间间隔。理想情况下,它也是对列进行排序的响应时间——这种情况下用户会感到他们正在给表格排序。
- 1秒:用户随意地在计算机指令空间进行操作而无需过度等待的感觉极限。0.2~1.0秒的延迟意味着会被用户注意到,因此感觉到计算机处于对指令的“处理中”,这有别于直接响应用户行为的指令。如:如果根据被选择的列对表格进行排序无法在0.1秒内完成,那么必须在1秒内完成,否则用户将感觉到UI变得缓慢且在执行任务中失去“流畅(flow)”的体验。超过1秒的延迟要提示用户计算机正在解决这个问题,如改变光标的形状。
- 10秒:用户专注于任务的极限。 超过10秒的任何操作都需要一个百分比完成指示器,以及一个方便用户中断操作且有清晰标识的方法。假设用户遭遇超过10秒延迟后才返回到原UI的情况,他们将需要重新适应。在用户的工作中,超过10的延迟仅在自然中断时可以接受,如切换任务时。
- 如果JS代码执行时间超过0.1秒,页面将会给人不够平滑快捷的感觉;执行时间超过1秒,则会感到应用程序缓慢;超过10秒,则用户将非常沮丧。
- 创建快速响应网页
- 1. 使用多线程(不可行)——JS是单线程
- Web Workers是一个功能强大的新工具,它可用于解除威胁到UI快速响应能力的复杂计算。当Web Workers不可用时,可以使用Gears插件和JS定时器。
- 2. 内存管理
- 内存管理不善会导致UI的性能问题
- 1. 使用多线程(不可行)——JS是单线程
- 拆分初始化负载
- 编写高效的JS
- 管理作用域
- 当执行JS代码时,JS引擎会创建一个执行上下文。执行上下文(作用域)设定了代码执行时所处的环境。JS引擎会在页面加载后创建一个全局的执行上下文,然后当执行一个函数时都会创建一个对应的执行上下文,最终建立一个执行上下文的堆栈,当前起作用的执行上下文在堆栈的最顶部。
- 每个执行上下文都有一个与之关联的作用域链,用于解析标识符。作用域链包含一个或多个变量对象,这些对象定义了执行上下文作用域内的标识符。全局执行上下文的作用域链中只有一个变量对象,它定义了JS中所有可用的全局变量和函数。当函数被创建(不是执行)时,JS引擎会把创建时执行上下文的作用域链赋给函数的内部属性【Scope】(内部属性不能通过JS来存取,所以无法直接访问此属性)。然后,当函数被执行时,JS引擎会创建一个活动对象(Activetion Object),并在初始化时给this、arguments、命名参数和该函数的所有局部变量赋值。活动对象会出现在执行上下文作用域链的顶端,紧接其后的是函数【Scope】属性中的对象。
- 在执行代码时,JS引擎通过搜索执行上下文的作用域链来解析诸如变量和函数名这样的标识符。解析标识符的过程从作用域链的顶部开始,按照自上而下的顺序进行。
- 理解JS中如何管理作用域和作用域链很重要,因为在作用域链中要查找的对象个数直接影响标识符解析的性能。标识符在作用域链中的位置越深,查找和访问它所需的时间就越长;如果作用域管理不当,就会给脚本的执行时间带来负面影响。
- 局部变量是JS中读写最快的标识符,因为它们存在于执行函数的活动对象中,解析标识符只需要查找作用域链中的单个对象。
- 读取变量值的总耗时随着查找作用域链的逐层深入而不断增长,所以标识符越深存取速度越慢。
- 注意:全局变量始终是作用域链中最后一个对象,所以对全局标识符的解析总是最耗时的。
- 在代码执行过程中,执行上下文对应的作用域链通常保持不变。然而有两个语句会临时增长执行上下文的作用域链。
- 1. with语句用于将对象属性作为局部变量来显示,使其便于访问,实际上它是将一个新的变量对象添加到执行上下文作用域链的顶部。当执行with语句中的代码时,函数中的局部变量将从作用域链的第一个对象变为第二个对象,会减慢标识符的存取。一旦with语句执行结束,作用域链将恢复到原来的状态。应避免使用with。
- 2. catch从句:在执行catch从句中的代码时,会在作用域链的顶部增加一个对象。该对象包含了由catch指定命名的异常对象。由于catch从句仅在执行try从句发生错误时才执行,所以它比with语句的影响要小,但应注意不要在catch从句中执行过多的代码,以将其带来的性能影响减小到最低。
- 管理好作用域链的深度,是一种只要少量工作就能提高性能的简易方法。我们要避免因不必要地增长作用域链而无意中导致执行速度变得缓慢。
- 高效数据存取
- 数据在脚本中存储的位置直接影响脚本执行的总耗时。
- 一般,在脚本中有4种地方可以存取数据
- 字面量值
- 变量
- 数组元素
- 对象属性
- 始终将那些需要频繁存取的值存储到局部变量中
- 随着数据结构深度的增加,它对数据存取速度的影响也跟着变大。
- 在数据存取时,将函数中使用超过一次的对象属性或数组元素存储为局部变量是一种好方法。
- 流控制
- 开发人员通过一系列条件和循环语句,精确地控制执行流从代码的一部分到另一部分。在每个环节上选择恰当的语句能够极大地提高脚本的运行速度。
- 当要查询的条件范围很大时,可以使用数组查询,因为它们不必检测上下边界。
- 字符串优化
- 避免运行时间过长的脚本
- 如果JS代码未经过细心的设计,有可能长时间地冻结页面,并最终导致浏览器停止响应。大多数浏览器会检测到长时间运行的脚本,并弹出中止脚本运行对话框询问用户是否允许脚本继续执行下去。
- 常见脚本执行时间过长原因
- 过多的DOM交互
- 过多的循环
- 过多的递归
- 管理作用域
- 客户端Comet的性能优化目的是:减少数据传输的延迟、HTTP连接的保存和管理、远程消息和处理跨域问题。服务器端的性能优化目的是:保存和共享HTTP的连接数,并尽量减少每个连接所消耗的内存、CPU、I/O和带宽。
- 少用iframe
- iframe是开销最高的DOM元素
- iframe阻塞onload事件。动态地设置iframe的src属性能在Safari和Chrome中避免这个问题,其他浏览器,可以在onload事件后设置src属性来避免。
- 通常情况下,iframe和主页面中的资源是并行下载的,但在某些情况下,主页面会阻塞iframe中资源的下载。
- 1.脚本位于iframe之前
- 2.样式表位于iframe之前
- 3.样式表位于iframe之后
- “足够快”的界定