笔记整理,看前方-项目优化

223 阅读21分钟

性能优化

本文主要考量客户端性能、服务器端和网络性能,内容框架来自 Yahoo Developer Network,包含 7 个类别共 35 条前端性能优化最佳实践,在此基础上补充了一些相关或者更符合主流技术的内容。

同时,建议关注及时更新的 Google 性能优化指南。

目录:

  • 页面内容
    • 减少 HTTP 请求数
    • 减少 DNS 查询
    • 避免重定向
    • 缓存策略
    • 延迟加载
    • 预先加载
    • 减少 DOM 元素数量
    • 划分内容到不同域名
    • 尽量减少 iframe 使用
    • 避免 404 错误
  • 服务器
    • 使用 CDN
    • 添加 Expires 或 Cache-Control 响应头
    • 启用 Gzip
    • 配置 Etag
    • 尽早输出缓冲
    • Ajax 请求使用 GET 方法
  • Cookie
    • 减少 Cookie 大小
    • 静态资源使用无 Cookie 域名
  • CSS
    • 把样式表放在 中
    • 不要使用 CSS 表达式
    • 使用 <link> 替代 @import
    • 不要使用 filter
  • JavaScript
    • 把脚本放在页面底部
    • 使用外部 JavaScript 和 CSS
    • 压缩 JavaScript 和 CSS
    • 移除重复脚本
    • 减少 DOM 操作
    • 使用高效的事件处理
  • 图片
    • 优化图片
    • 优化 CSS Sprite
    • 不要在 HTML 中缩放图片
    • 使用体积小、可缓存的 favicon.ico
    • 避免图片 src 为空

来自 Google 的数据表明,一个有 10 条数据 0.4 秒能加载完的页面,变成 30 条数据 0.9 秒加载完之后,流量和广告收入下降 90%。
Google Map 首页文件大小从 100KB 减小到 70-80KB 后,流量在第一周涨了 10%,接下来的三周涨了 25%。
亚马逊的数据表明:加载时间增加 100 毫秒,销量就下降 1%。
以上数据更说明「加载时间就是金钱」,前端优化主要围绕提高加载速度进行。

1.页面内容

1.1.减少 HTTP 请求数

Web 前端 80% 的响应时间花在图片、样式、脚本等资源下载上。浏览器对每个域名的连接数是有限制的,减少请求次数是缩短响应时间的关键。

通过简洁的设计减少页面所需资源,进而减少 HTTP 请求,这是最直接的方式,前提是你的 Boss、设计师同事不打死你。所以,还是另辟蹊径吧:

  • 合并 JavaScript、CSS 等文件;
    • 服务器端(CDN)自动合并
    • 基于 Node.js 的文件合并工具一抓一大把
  • 使用CSS Sprite:将背景图片合并成一个文件,通过background-image 和 background-position 控制显示;
    • Sprite Cow
    • Spritebox

逐步被 Icon Font 和 SVG Sprite 取代。

  • Image Map:合并图片,然后使用坐标映射不同的区域(演示)。

缺点:仅适用于相连的图片;设置坐标过程乏味且易出错;可访性问题。不推荐使用这种过时的技术。

  • Inline Assets:使用 Data URI scheme 将图片嵌入 HTML 或者 CSS 中;或者将 CSS、JS、图片直接嵌入 HTML 中。

会增加文件大小,也可能产生浏览器兼容及其他性能问题(有待整理补充)。 未来的趋势是使用内嵌 SVG。

  • 内容分片,将请求划分到不同的域名上。

HTTP/2 通过多路复用大幅降低了多个请求的开销。通过数据分帧层,客户端和服务器之间只需要建立一个 TCP 连接,即可同时收发多个文件,而且,该连接在相当长的时间周期内保持打开(持久化),以便复用。

HTTP/2 的新特性意味着上述优化实践不再适用,但考虑到客户端对 HTTP/2 的支持覆盖程度,还需根据实际数据权衡。

1.2.减少 DNS 查询

用户输入 URL 以后,浏览器首先要查询域名(hostname)对应服务器的 IP 地址,一般需要耗费 20-120 毫秒 时间。DNS 查询完成之前,浏览器无法从服务器下载任何数据。

基于性能考虑,ISP、局域网、操作系统、浏览器都会有相应的 DNS 缓存机制。

  • IE 缓存 30 分钟,可以通过注册表中 DnsCacheTimeout 项设置;
  • Firefox 缓存 1 分钟,通过 network.dnsCacheExpiration 配置;

首次访问、没有相应的 DNS缓存时,域名越多,查询时间越长。所以应尽量减少域名数量。但基于并行下载考虑,把资源分布到 2 个域名上(最多不超过 4 个)。这是减少 DNS 查询同时保证并行下载的折衷方案。

1.3.避免重定向

HTTP 重定向通过 301/302 状态码实现。

HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html

客户端收到服务器的重定向响应后,会根据响应头中 Location 的地址再次发送请求。重定向会影响用户体验,尤其是多次重定向时,用户在一段时间内看不到任何内容,只看到浏览器进度条一直在刷新。

有时重定向无法避免,在糟糕也比抛出 404 好。虽然通过 HTML meta refresh 和 JavaScript 也能实现,但首选 HTTP 3xx 跳转,以保证浏览器「后退」功能正常工作(也利于 SEO)。

  • 最浪费的重定向经常发生、而且很容易被忽略:URL 末尾应该添加 / 但未添加。比如,访问 http://astrology.yahoo.com/astrology 将被 301 重定向到 http://astrology.yahoo.com/astrology/(注意末尾的 /)。如果使用 Apache,可以通过 Alias 或 mod_rewriteDirectorySlash 解决这个问题。
  • 网站域名变更:CNAME 结合 Aliasmod_rewrite 或者其他服务器类似功能实现跳转。

1.4.缓存策略

Ajax 可以提高用户体验。但「异步」不意味着「及时」,优化 Ajax 响应速度提高性能仍是需要关注的主题。

最重要的的优化方式是缓存响应结果,详见 添加 ExpiresCache-Control 响应头。

以下规则也关乎 Ajax 响应速度:

  • 启用 Gzip
  • 减少 DNS 查询
  • 压缩 JavaScript 和 CSS
  • 避免重定向
  • 配置 Etag

1.5.延迟加载

页面初始加载时哪些内容是绝对必需的?不在答案之列的资源都可以延迟加载。比如:

  • 非首屏使用的数据、样式、脚本、图片等;
  • 用户交互时才会显示的内容。
  • 遵循「渐进增强」理念开发的网站:JavaScript 用于增强用用户体验,但没有(不支持) JavaScript 也能正常工作,完全可以延迟加载 JavaScript。

延迟渲染
将首屏以外的 HTML 放在不渲染的元素中,如隐藏的 <textarea>,或者 type 属性为非执行脚本的 <script> 标签中,减少初始渲染的 DOM 元素数量,提高速度。等首屏加载完成或者用户操作时,再去渲染剩余的页面内容。

1.6.预先加载

预先加载利用浏览器空闲时间请求将来要使用的资源,以便用户访问下一页面时更快地响应。

  • 无条件预先加载:页面加载完成(load)后,马上获取其他资源。以 google.com 为例,首页加载完成后会立即下载一个 Sprite 图片,此图首页不需要,但是搜索结果页要用到。

  • 有条件预先加载:根据用户行为预判用户去向,预载相关资源。比如 search.yahoo.com 开始输入时会有额外的资源加载。

Chrome 等浏览器的地址栏也有类似的机制。

  • 有「阴谋」的预先加载:页面即将上线新版前预先加载新版内容。网站改版后由于缓存、使用习惯等原因,会有旧版的网站更快更流畅的反馈。为缓解这一问题,在新版上线之前,旧版可以利用空闲提前加载一些新版的资源缓存到客户端,以便新版正式上线后更快的载入(好一个「心机猿」:scream:)。

「双十一」、「黑五」这类促销日来临之前,也可以预先下载一些相关资源到客户端(浏览器、App 等),有效利用浏览器缓存和本地存储,降低活动当日请求压力,提高用户体验。

  • Resource Hints Spec
    Resource Hints 是非常好的一种性能优化方法,可以大大降低页面加载时间,给用户更加流畅的用户体验。
    现代浏览器使用大量预测优化技术来预测用户行为和意图,这些技术有预连接、资源与获取、资源预渲染等。
    Resource Hints 的思路有如下两个:

    • 当前将要获取资源的列表
    • 通过当前页面或应用的状态、用户历史行为或 session 预测用户行为及必需的资源

实现Resource Hints的方法有很多种,可分为基于 link 标签的 DNS-prefetchsubresourcepreloadprefetchpreconnectprerender,和本地存储 localStorage

详细可参考前端性能优化 - Resource Hints 资源预加载

1.7.减少 DOM 元素数量

复杂的页面不仅下载的字节更多,JavaScript DOM 操作也更慢。例如,同是添加一个事件处理器,500 个元素和 5000 个元素的页面速度上会有很大区别。

从以下几个角度考虑移除不必要的标记:

  • 是否还在使用表格布局?
  • 塞进去更多的
    仅为了处理布局问题?也许有更好、更语义化的标记。
  • 能通过伪元素实现的功能,就没必要添加额外元素,如清除浮动。

浏览器控制台中输入以下代码可以计算出页面中有多少 DOM 元素:

document.getElementsByTagName('*').length;

对比标记良好的的网站,看看差距是多少。

1.8.划分内容到不同域名

浏览器一般会限制每个域的并行线程(一般为6个,甚至更少),使用不同的域名可以最大化下载线程,但注意保持在 2-4 个域名内,以避免 DNS 查询损耗。

例如,动态内容放在 csspod.com 上,静态资源放在 static.csspod.com 上。这样还可以禁用静态资源域下的 Cookie,减少数据传输,详见 Cookie 优化。

1.9.尽量减少 iframe 使用

使用 iframe 可以在页面中嵌入 HTML 文档,但有利有弊。

<iframe> 优点:

  • 可以用来加载速度较慢的第三方资源,如广告、徽章;
  • 可用作安全沙箱;
  • 可以并行下载脚本。

<iframe> 缺点:

  • 加载代价昂贵,即使是空的页面;

  • 阻塞页面 load 事件触发;

  • Iframe 完全加载以后,父页面才会触发 load 事件。 Safari、Chrome 中通过 JavaScript 动态设置 iframe src 可以避免这个问题。

  • 缺乏语义。

1.10.避免 404 错误

HTTP 请求很昂贵,返回无效的响应(如 404 未找到)完全没必要,降低用户体验而且毫无益处。

一些网站设计很酷炫、有提示信息的 404 页面,有助于提高用户体验,但还是浪费服务器资源。尤其糟糕的是外部脚本返回 404,不仅阻塞其他资源下载,浏览器还会尝试把 404 页面内容当作 JavaScript 解析,消耗更多资源。

2.服务器

2.1.使用 CDN

网站 80-90% 响应时间消耗在资源下载上,减少资源下载时间是性能优化的黄金原则

相比分布式架构的复杂和巨大投入,静态内容分发网络(CDN)可以以较低的投入,获得加载速度有效提升。

2.2.添加 ExpiresCache-Control 响应头

  • 静态内容:将 Expires 响应头设置为将来很远的时间,实现「永不过期」策略;
  • 动态内容:设置合适的 Cache-Control 响应头,让浏览器有条件地发起请求。 Cache-Control 头在 HTTP/1.1 规范中定义,取代了之前用来定义响应缓存策略的头(例如 Expires、Pragma)。当前的所有浏览器都支持 Cache-Control,因此,使用它就够了。

鉴于静态内容和动态内容不同的缓存策略,实践中一般会把二者部署在不同的服务器(域名)以方便管理。

2.3.启用 Gzip

Gzip 压缩通常可以减少 70% 的响应大小,对某些文件更可能高达 90%,比 Deflate 更高效。主流 Web 服务器都有相应模块,而且绝大多数浏览器支持 gzip 解码。所以,应该对 HTML、CSS、JS、XML、JSON 等文本类型的内容启用压缩。

注意,图片和 PDF 文件不要使用 gzip。它们本身已经压缩过,再使用 gzip 压缩不仅浪费 CPU 资源,而且还可能增加文件体积。

对于不支持的 Gzip 的用户代理,通过设置 Vary 响应头,返回为未压缩的数据:

Vary: *

2.4.配置 Etag

Etag 通过文件版本标识,方便服务器判断请求的内容是否有更新,如果没有就响应 304,避免重新下载。

当然,启用 Etag 可能会导致其他问题,还需要根据具体情况做判断。

2.5.Ajax 请求使用 GET 方法

浏览器执行 XMLHttpRequest POST 请求时分成两步,先发送 Header,再发送数据。而 GET 只使用一个 TCP 数据包发送数据,所以首选 GET 方法。

根据 HTTP 规范,GET 用于获取数据,POST 则用于向服务器发送数据,所以 Ajax 请求数据时使用 GET 更符合规范(GET 和 POST 对比)。

IE 中最大 URL 长度为 2K,如果超出 2K,则需要考虑使用 POST 方法。

3.Cookie

3.1.减少 Cookie 大小

Cookie 被用于身份认证、个性化设置等诸多用途。Cookie 通过 HTTP 头在服务器和浏览器间来回传送,减少 Cookie 大小可以降低其对响应速度的影响。

  • 去除不必要的 Cookie;
  • 尽量压缩 Cookie 大小;
  • 注意设置 Cookie 的 domain 级别,如无必要,不要影响到 sub-domain;
  • 设置合适的过期时间。

3.2 静态资源使用无 Cookie 域名

静态资源一般无需使用 Cookie,可以把它们放在使用二级域名或者专门域名的无 Cookie 服务器上,降低 Cookie 传送的造成的流量浪费,提高响应速度。

4.CSS

4.1.把样式表放在 <head>

把样式表放在 <head> 中可以让页面渐进渲染,尽早呈现视觉反馈,给用户加载速度很快的感觉。

这对内容比较多的页面尤为重要,用户可以先查看已经下载渲染的内容,而不是盯着白屏等待。

如果把样式表放在页面底部,一些浏览器为减少重绘,会在 CSS 加载完成以后才渲染页面,用户只能对着白屏干瞪眼,用户体验极差。

4.2使用<link> 替代 @import

加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载。

5.JavaScript

5.1.把脚本放在页面底部

浏览器下载脚本时,会阻塞其他资源并行下载,即使是来自不同域名的资源。因此,最好将脚本放在底部,以提高页面加载速度。

一些特殊场景无法将脚本放到页面底部的,可以考虑 <script> 的以下属性:

  • defer 属性;
  • HTML5 新增的 async 属性。

5.2.使用外部 JavaScript 和 CSS

外部 JavaScript 和 CSS 文件可以被浏览器缓存,在不同页面间重用,也能降低页面大小。

当然,实际中也需要考虑代码的重用程度。如果仅仅是某个页面使用到的代码,可以考虑内嵌在页面中,减少 HTTP 请求数。另外,可以在首页加载完成以后,预先加载子页面的资源。

5.3.压缩 JavaScript 和 CSS

压缩代码可以移除非功能性的字符(注释、空格、空行等),减少文件大小,提高载入速度。

得益于 Node.js 的流行,开源社区涌现出许多高效、易用的前端优化工具,JavaScript 和 CSS 压缩类的,不敢说多如牛毛,多入鸡毛倒是一点不夸张,如 [UglifyJS 2] (github.com/mishoo/Ugli…)、csso、cssnano 等。

对于内嵌的 CSS 和 JavaScript,也可以通过 htmlmin 等工具压缩。

这些项目都有 Gulp、Webpack 等流行构建工具的配套版本。

5.4.减少 DOM 操作

JavaScript 操作 DOM 很慢,尤其是 DOM 节点很多时。

使用时应该注意:

  • 缓存已经访问过的元素;
  • 使用 DocumentFragment 暂存 DOM,整理好以后再插入 DOM 树;
  • 操作 className,而不是多次读写 style;
  • 避免使用 JavaScript 修复布局。

5.5.使用高效的事件处理

  • 减少绑定事件监听的节点,如通过事件委托;
  • 尽早处理事件,在 DOMContentLoaded 即可进行,不用等到 load 以后。

对于 resize、scroll 等触发频率极高的事件,应该通过 debounce 等机制降低处理程序执行频率。

6.图片

6.1.优化图片

6.2.优化 CSS Sprite

  • 水平排列 Sprite 中的图片,垂直排列会增加图片大小;
  • Spirite 中把颜色较近的组合在一起可以降低颜色数,理想状况是低于 256 色以适用 PNG8 格式;
  • 不要在 Spirite 的图像中间留有较大空隙。减少空隙虽然不太影响文件大小,但可以降低用户代理把图片解压为像素图的内存消耗,对移动设备更友好。

6.3.不要在 HTML 中缩放图片

不要使用 <img> 的 width、height 缩放图片,如果用到小图片,就使用相应大小的图片。

很多 CMS 和 CDN 都提供图片裁切功能。

6.4.使用体积小、可缓存的 favicon.ico

Favicon.ico 一般存放在网站根目录下,无论是否在页面中设置,浏览器都会尝试请求这个文件。

所以确保这个图标:

  • 存在(避免 404);
  • 尽量小,最好小于 1K;
  • 设置较长的过期时间。
  • 对于较新的浏览器,可以使用 PNG 格式的 favicon。

设置图片的宽和高,以免浏览器按照「猜」的宽高给图片保留的区域和实际宽高差异,产生重绘。

6.5.避免图片 src 为空

图片 src 属性值为空字符串可能以下面两种形式出现:

HTML:

<img src="" />

JavaScript:


var img = new Image(); 
img.src = "";

虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求:

  • IE 向页面所在的目录发送请求;
  • Safari、Chrome、Firefox 向页面本身发送请求;
  • Opera 不执行任何操作。

以上数据较老,当下主流版本可能会有改变。

空 src 产生请求的后果不容小觑:

  • 给服务器造成意外的流量负担,尤其时日 PV 较大时;
  • 浪费服务器计算资源;
  • 可能产生报错。

空的 href 属性也存在类似问题。用户点击空链接时,浏览器也会向服务器发送 HTTP 请求,可以通过 JavaScript 阻止空链接的默认的行为。

错误/性能监控

编写代码只是做好项目的一小部分,写代码难免会碰到错误。因此,在项目上线后,我们还需要主动对项目的错误进行收集,不能等用户发现错误,再联系我们,我们再去处理。这样很容易造成大的损失,提前做好错误收集和处理,可以减少损失。

1.收集哪些错误信息

先从一个面试题开始吧。腾讯第二轮电话面试的一个题目:如果用户使用网页,发现白屏,现在联系上了你们,你们会向他询问什么信息呢?

一个个去堆答案没有意思,我们换个思路,先想一下为什么会白屏?

错误发生在什么环节

跟我之前的性能优化的文章一样,我们以用户访问页面的过程为顺序,大致排查一下

  • 用户没打开网络
  • DNS域名劫持
  • http劫持
  • cdn或是其他资源文件访问出错
  • 服务器错误
  • 接口出错
  • 前端代码错误
  • 浏览器兼容性问题(css或是js)
  • 设备屏幕尺寸兼容问题
  • 用户操作出错

收集哪些信息

通过以上可能发生错误的环节,我们需要向用户手机一下以下的用户信息

  • 当前的网络状态
  • 运营商
  • 地理位置
  • 访问时间
  • 客户端的版本(如果是通过客户端访问)
  • 系统版本
  • 浏览器信息
  • 设备分辨率
  • 页面的来源
  • 用户账号信息
  • 页面访问流程各阶段耗时
  • js代码报错信息

另外,参考Google Analytics数据统计的原理,我们还可以收集下面的信息

  • 用户的浏览器语言编码
  • 浏览器语言设置
  • flash的版本
  • 网页的标题
  • 网页的来源
  • cookie的数据

如何收集错误的信息

现在话题来到了如何收集错误信息了。

前端错误收集有两大流派:

虚拟机监控

  • 一个是虚拟机监控,优点是指标齐全,并且可以进行竞品监控,缺点是反映不全,容易失真

下面简单列出一些虚拟机监控的方案

1. 图片对比,每隔一段时间使用虚拟机访问页面并截图然后进行相似度对比,如果差值超出一定的数值,进行报警
2. 定时抓取页面的HTML源码,分析是否出现异常

脚本监控

  • 另一个是脚本监控,优点是可以收集海量真实数据,缺点是影响性能,采样少的情况下容易失真。

这里暂时只重点讲脚本监控

在这里,我们主要利用脚本收集用户的各阶段访问消耗时间以及错误信息

各阶段访问耗时记录

之所以要收集访问耗时,是为了查出是什么阶段消耗的时间较多,从而更好的定位错误来源。

首先我们可以关注一下Performance,下面就先讲一下其中的两个API

performance timing

在chrome浏览器控制台输入Performance.timing,会得到记录了一个浏览器访问各阶段的时间的对象。

进行错误收集的时候,可以对比这些时间,看错误发生在什么阶段

  • DNS 查询耗时 :domainLookupEnd - domainLookupStart
  • TCP 链接耗时 :connectEnd - connectStart
  • request 请求耗时 :responseEnd - responseStart
  • 解析 dom 树耗时 : domComplete - domInteractive
  • 白屏时间 :responseStart - navigationStart
  • domready 时间 :domContentLoadedEventEnd - navigationStart
  • onload 时间 :loadEventEnd – navigationStart

performance getEntries

通过performance.getEntries(),可以得到一个数字,其中的每个元素分别代表着一个资源,元素对象包括的属性跟上面的performance timing有点接近,还有不同的属性包括name代表资源的地址,请求花费的时间duration。

其他方法

记录访问开始的时间可有以下的方法:

  1. 服务器将访问的时间渲染到页面上
  2. SPA的话,记录前一个页面卸载的时间

记录访问过程的时间

  1. 在head标签解析后,渲染body标签前加入script标签进行打点,一般将这个时间视为白屏时间
  2. 捕获DOMContentLoaded事件来记录dom元素加载完毕的时间
  3. 在首屏页面的所有图片加载完后进行记录,保存首屏时间
  4. 捕获load事件记录页面加载完成的时间

脚本错误信息收集

window.onerror

window.onerror可以捕捉运行时错误,可以拿到出错的信息,堆栈,出错的文件、行号、列号

要注意以下几点:

  • 要把window.onerror这个代码块分离出去,并且比其他脚本先执行(注意这个前提!)即可捕捉到语法错误。

  • 由于网络请求异常事件不会冒泡,需要在捕获阶段进行处理

  • 不能捕获promise的错误信息

  • 跨域资源需要专门处理,需要在script标签加上crossorigin属性,服务器设置Access-Control-Allow-Origin

window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx

promise的错误处理

promise除了使用catch方法来捕获错误,还可以使用window的unhandledrejection事件捕获异常的

window.addEventListener("unhandledrejection", function(e){
  // Event新增属性
  // @prop {Promise} promise - 状态为rejected的Promise实例
  // @prop {String|Object} reason - 异常信息或rejected的内容
  // 会阻止异常继续抛出,不让Uncaught(in promise) Error产生
  e.preventDefault()
})

try catch

无法捕捉到语法错误,只能捕捉运行时错误;
可以拿到出错的信息,堆栈,出错的文件、行号、列号; 需要借助工具把所有的function块以及文件块加入try,catch,可以在这个阶段打入更多的静态信息。

要注意的是try catch只能捕获同步代码的异常,对回调,setTimeout,promise等无能为力

上报错误的方式

  1. 后端提供接口,前端ajax上传

  2. 创建一个新的图片,url参数带上错误信息

使用创建图片的好处是简单方便可跨域,可以防止重复请求,但要注意到地址栏可以携带的信息有限。

function report(error) {
  var reportUrl = 'http://xxxx/report';
  new Image().src = reportUrl + 'error=' + error;
}

项目持续集成和部署

内容转自:

前端性能优化最佳实践

前端错误监控与收集探究