关于性能优化类问题的回答

回答一:

  1. 这性能优化呢,它是一个特别大的方向,因为每个项目可能优化的点都不一样,每一种框架或者每一种客户端可以优化的点也都不一样。

总的来说,现在 B/S 架构下都是前端向后端请求,后端整理好数据给客户端返回,然后客户端再进行数据处理、到渲染将界面展示出来,这么一个大致流程。

那我们优化就是要基于这一过程,说白了我们能够优化的点,就只有两个大的方向。

  • 一是更快的网络通信,就是客户端和服务端之间,请求或响应时 数据在路上的时间让它更快

  • 二是更快的数据处理,指的是

服务器接到请求之后,更快的整理出客户端需要的数据

客户端收到响应的数据后,更快的给用户展示 以及 交互时更快的处理

  1. 更快网络通信方面比如:CDN 做全局负载均衡、CDN 缓存、域名分片、资源合并、雪碧图、字体图标、http 缓存,以减少请求;还有 gzip/br 压缩、代码压缩、减少头信息、减少 Cookie、使用 http2、用 jpg/webp、去除元数据等等。

更快数据处理方面比如:SSR、SSG、预解析、懒加载、按需引入、按需加载、CSS 放上面、JS 放下面、语义化标签、动画能用 CSS 就不用 JS、事件委托、减少重排、等等代码优化。

  1. 总结就是请求优化、代码优化、打包优化都是常规操作等等。

因为每个项目优化的点可能都不一样,所以优化主要 还是根据自己的项目来。具体要优化什么主要还是看浏览器(Chrome为例)开发者工具里的LighthousePerformance

# 一. lighthouse

# (一)基本使用

  • 直接在 Chrome 开发者工具中打开

  • 或者 Node 12.x以上的版本可以直接安装在本地

npm install -g lighthouse

  • 安装好后,比如生成掘金的性能分析报告,一句代码就够了,然后就会生成一个html文件

lighthouse https://juejin.cn/

# (二)五大性能指标

  1. Performance:这个又分为三块性能指标、可优化的项、和手动诊断

  2. Accessibility:无障碍功能分析。比如前景色和背景色没有足够对比度、图片有没有 alt 属性、a 链接有没有可识别名称等等

  3. Best Practices:最佳实践策略。比如图片纵横比不正确,控制台有没有报错信息等

  4. SEO:有没有 SEO 搜索引擎优化的一些东西

  5. PWA:官方说法是衡量站点是否快速、可靠和可安装。在国内浏览器内核不统一,小程序又这么火,所以好像没什么落地的场景

# 二. Performance

它记录了网站运行过程中性能数据。我们回放整个页面执行过程的话,就可以定位和诊断每个时间段内页面运行情况,不过它没有性能评分,也没有优化建议,只是将采集到的数据按时间线的方式展现。

打开Performance,勾选 Memory,点击左边的 Record 开始录制,然后执行操作或者刷新页面,然后再点一下(Stop)就结束录制,生成数据。

生成数据:主要有概览面板,性能面板和详情面板。

  1. 概况面板

里面有页面帧速(FPS)、白屏时间、CPU 资源消耗、网络加载情况、V8 内存使用量(堆)等等,按时间顺序展示。

  • 如果 FPS(看图右上角)图表上出现红色块,就表示红色块附近渲染出一帧的时间太长了,就有可能导致卡顿。

  • 如果 CPU 图形占用面积太大,表示 CPU 使用率高,就可能是因为某个 JS 占用太多主线程时间,阻塞其他任务执行。

  • 如果 V8 的内存使用量一直在增加,就可能因为某种原因导致内存泄露。

    • 一次查看内存占用情况后,看当前内存占用趋势图,走势呈上升趋势,可以认为存在内存泄露

    • 多次查看内存占用情况后截图对比,比较每次内存占用情况,如果呈上升趋势,也可以认为存在内存泄露

  • 通过概览面板定位到可能存在问题的时间节点后,点击时间线上有问题的地方,然后这一块详细内容就会显示在性能面板中,这样拿到更进一步的数据来分析导致该问题的直接原因。

  1. 性能面板 比如我们点击时间线上的某个位置(如红色块),性能面板就会显示该时间节点内的性能数据,如图

  • Main 指标:是渲染主线程的任务执行记录

  • Timings 指标:记录如 FP、FCP、LCP 等产生一些关键时间点的数据信息

  • Interactions 指标:记录用户交互操作

  • Network 指标:是页面每个请求所耗时间

  • Compositor 指标:是合成线程的任务执行记录

  • GPU 指标:是 GPU 进程的主线程的任务执行记录

  • Chrome_ChildIOThread 指标:是 IO 线程的任务执行记录,里面有用户输入事件,网络事件,设备相关等事件

  • Frames 指标:记录每一帧的时间、图层构造等信息

(1)Main 指标

性能指标项有很多,而我们使用的时候多数时间都是分析 Main 指标,如图

上面第一行灰色的,写着 Task 的,一个 Task 就是一个任务

下面黄色紫色的都是啥呢,那是一个任务里面的子任务

放大,举个例子

Task 是一个任务,下面的就是 Task 里面的子任务,这个图用代码表示就是

function A() {
  A1();
  A2();
}
function Task() {
  A();
  B();
}
Task();
1
2
3
4
5
6
7
8
9

所以我们就可以选中标红的 Task ,然后放大(滑动鼠标就可放大),看里面具体的耗时点。

比如都做了哪些操作,哪些函数耗时了多少,代码有压缩的话看到的就是压缩后的函数名。然后我们点击一下某个函数,在面板最下面,就会出现代码的信息,是哪个函数,耗时多少,在哪个文件上的第几行等。这样我们就很方便地定位到耗时函数了

还可以横向切换 tab ,看它的调用栈等情况,更方便地找到对应代码

(2)Timings 指标

Timings 指标也需要注意,如图

  • FP:表示首次绘制。记录页面第一次绘制像素的时间

  • FCP:表示首次内容绘制(只有文本、图片(包括背景图)、非白色的 canvas 或 SVG 时才被算作 FCP)

  • LCP:最大内容绘制,是代表页面的速度指标。记录视口内最大元素绘制时间,这个会随着页面渲染变化而变化

  • FID:首次输入延迟,代表页面交互体验的指标。记录 FCP 和 TTI 之间用户首次交互的时间到浏览器实际能够回应这种互动的时间

  • CLS:累计位移偏移,代表页面稳定的指标。记录页面非预期的位移,比如渲染过程中插入一张图片或者点击按钮动态插入一段内容等,这时候就会触发位移

  • TTI:首次可交互时间。指在视觉上已经渲染了,完全可以响应用户的输入了。是衡量应用加载所需时间并能够快速响应用户交互的指标。与 FMP 一样,很难规范化适用于所有网页的 TTI 指标定义

  • DCL: 表示 HTML 加载完成时间

注意:DCLL表示的时间在 PerformanceNetWork 中是不同的,因为 Performance 的起点是点击录制的时间,Network 中起点时间是 fetchStart时间(检查缓存之前,浏览器准备好使用 http 请求页面文档的时间)

  • L:表示页面所有资源加载完成时间

  • TBT:阻塞总时间。记录 FCPTTI 之间所有长任务的阻塞时间总和

  • FPS:每秒帧率。表示每秒钟画面更新次数,现在大多数设备是60 帧/秒

  • FMP:首次有意义的绘制。是页面主要内容出现在屏幕上的时间,这是用户感知加载体验的主要指标。有点抽象,因为目前没有标准化的定义。因为很难用通用的方式来确定各种类型的页面的关键内容

  • FCI:首次 CPU空闲时间。表示网页已经满足了最小程度的与用户发生交互行为的时间

然后根据指标体现出来的问题,有针对性的优化即可

# 三. 雅虎 35 条军规

点击查看

# 内容部分

1. 尽量减少 HTTP 请求数

80%的终端用户响应时间都花在了前端上,其中大部分时间都在下载页面上的各种组件:图片,样式表,脚本,Flash 等等。减少组件数必然能够减少页面提交的 HTTP 请求数。这是让页面更快的关键。

减少页面组件数的一种方式是简化页面设计。但有没有一种方法可以在构建复杂的页面同时加快响应时间呢?嗯,确实有鱼和熊掌兼得的办法。

合并文件是通过把所有脚本放在一个文件中的方式来减少请求数的,当然,也可以合并所有的 CSS。如果各个页面的脚本和样式不一样的话,合并文件就是一项比较麻烦的工作了,但把这个作为站点发布过程的一部分确实可以提高响应时间。

CSS Sprites是减少图片请求数量的首选方式。把背景图片都整合到一张图片中,然后用 CSS 的 background-imagebackground-position属性来定位要显示的部分。

图像映射可以把多张图片合并成单张图片,总大小是一样的,但减少了请求数并加速了页面加载。图片映射只有在图像在页面中连续的时候才有用,比如导航条。给 image map 设置坐标的过程既无聊又容易出错,用image map来做导航也不容易,所以不推荐用这种方式。

行内图片(Base64 编码)用 data: URL 模式来把图片嵌入页面。这样会增加 HTML 文件的大小,把行内图片放在(缓存的)样式表中是个好办法,而且成功避免了页面变“重”。但目前主流浏览器并不能很好地支持行内图片。

减少页面的 HTTP 请求数是个起点,这是提升站点首次访问速度的重要指导原则。

2.减少 DNS 查找

域名系统建立了主机名和 IP 地址间的映射,就像电话簿上人名和号码的映射一样。当你在浏览器输入www.yahoo.com 的时候,浏览器就会联系 DNS 解析器返回服务器的 IP 地址。DNS 是有成本的,它需要 20 到 120 毫秒去查找给定主机名的 IP 地址。在 DNS 查找完成之前,浏览器无法从主机名下载任何东西。

DNS 查找被缓存起来更高效,由用户的 ISP(网络服务提供商)或者本地网络存在一个特殊的缓存服务器上,但还可以缓存在个人用户的计算机上。DNS 信息被保存在操作系统的 DNS cache(微软 Windows 上的“DNS 客户端服务”)里。大多数浏览器有独立于操作系统的自己的cache。只要浏览器在自己的 cache 里还保留着这条记录,它就不会向操作系统查询 DNS。

IE 默认缓存 DNS 查找 30 分钟,写在DnsCacheTimeout注册表设置中。Firefox 缓存 1 分钟,可以用 network.dnsCacheExpiration配置项设置。(Fasterfox把缓存时间改成了 1 小时 ;FasterfoxFF 的一个提速插件)

如果客户端的 DNS cache是空的(包括浏览器的和操作系统的),DNS 查找数等于页面上不同的主机名数,包括页面 URL,图片,脚本文件,样式表,Flash 对象等等组件中的主机名,减少不同的主机名就可以减少 DNS 查找。

减少不同主机名的数量同时也减少了页面能够并行下载的组件数量,避免 DNS 查找削减了响应时间,而减少并行下载数量却增加了响应时间。我的原则是把组件分散在 2 到 4 个主机名下,这是同时减少 DNS 查找和允许高并发下载的折中方案。

3.避免重定向

重定向用 301302状态码,下面是一个有 301 状态码的 HTTP 头:

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

浏览器会自动跳转到Location 域指明的 URL。重定向需要的所有信息都在 HTTP 头部,而响应体一般是空的。其实额外的 HTTP 头,比如ExpiresCache-Control 也表示重定向。除此之外还有别的跳转方式:refresh元标签和 JavaScript,但如果你必须得做重定向,最好用标准的 3xxHTTP状态码,主要是为了让返回按钮能正常使用。

牢记重定向会拖慢用户体验,在用户和 HTML 文档之间插入重定向会延迟页面上的所有东西,页面无法渲染,组件也无法开始下载,直到 HTML 文档被送达浏览器。

有一种常见的极其浪费资源的重定向,而且 web 开发人员一般都意识不到这一点,就是 URL 尾部缺少一个斜线的时候。例如,跳转到http://astrology.yahoo.com/astrology会返回一个重定向到http://astrology.yahoo.com/astrology/的 301 响应(注意添在尾部的斜线)。在 Apache 中可以用 Aliasmod_rewrite 或者 DirectorySlash指令来取消不必要的重定向。

重定向最常见的用途是把旧站点连接到新的站点,还可以连接同一站点的不同部分,针对用户的不同情况(浏览器类型,用户帐号类型等等)做一些处理。用重定向来连接两个网站是最简单的,只需要少量的额外代码。虽然在这些时候使用重定向减少了开发人员的开发复杂度,但降低了用户体验。一种替代方案是用 Aliasmod_rewrite,前提是两个代码路径都在相同的服务器上。如果是因为域名变化而使用了重定向,就可以创建一条 CNAME(创建一个指向另一个域名的 DNS 记录作为别名)结合Alias或者 mod_rewrite 指令。

4.让 Ajax 可缓存

Ajax 的一个好处是可以给用户提供即时反馈,因为它能够从后台服务器异步请求信息。然而,用了 Ajax 就无法保证用户在等待异步 JavaScriptXML 响应返回期间不会非常无聊。在很多应用程序中,用户能够一直等待取决于如何使用 Ajax。例如,在基于 web 的电子邮件客户端中,用户为了寻找符合他们搜索标准的邮件消息,将会保持对 Ajax 请求返回结果的关注。重要的是,要记得“异步”并不意味着“即时”。

要提高性能,优化这些 Ajax 响应至关重要。最重要的提高 Ajax 性能的方法就是让响应变得可缓存,就像在添上 Expires或者Cache-Control HTTP 头中讨论的一样。下面适用于 Ajax 的其它规则:

  • Gzip组件
  • 减少 DNS查找
  • 压缩JavaScript
  • 避免重定向
  • 配置 ETags

我们一起看看例子,一个 Web 2.0 的电子邮件客户端用了 Ajax 来下载用户的通讯录,以便实现自动完成功能。如果用户从上一次使用之后再没有修改过她的通讯录,而且 Ajax 响应是可缓存的,有尚未过期的 Expires 或者 Cache-Control HTTP 头,那么之前的通讯录就可以从缓存中读出。必须通知浏览器,应该继续使用之前缓存的通讯录响应,还是去请求一个新的。可以通过给通讯录的Ajax URL里添加一个表明用户通讯录最后修改时间的时间戳来实现,例如&t=1190241612。如果通讯录从上一次下载之后再没有被修改过,时间戳不变,通讯录就将从浏览器缓存中直接读出,从而避免一次额外的 HTTP 往返消耗。如果用户已经修改了通讯录,时间戳也可以确保新的 URL 不会匹配缓存的响应,浏览器将请求新的通讯录条目。

即使 Ajax 响应是动态创建的,而且可能只适用于单用户,它们也可以被缓存,而这样会让你的 Web 2.0 应用更快。

5.延迟加载组件

可以凑近看看页面并问自己:什么才是一开始渲染页面所必须的?其余内容都可以等会儿。

JavaScript 是分隔onload 事件之前和之后的一个理想选择。例如,如果有 JavaScript 代码和支持拖放以及动画的库,这些都可以先等会儿,因为拖放元素是在页面最初渲染之后的。其它可以延迟加载的部分包括隐藏内容(在某个交互动作之后才出现的内容)和折叠的图片。

工具可帮你减轻工作量:YUI Image Loader 可以延迟加载折叠的图片,还有 YUI Get utility是一种引入 JS 和 CSS 的简单方法。Yahoo!主页就是一个例子,可以打开 Firebug 的网络面板仔细看看。

最好让性能目标符合其它 web 开发最佳实践,比如“渐进增强”。如果客户端支持 JavaScript,可以提高用户体验,但必须确保页面在不支持 JavaScript 时也能正常工作。所以,在确定页面运行正常之后,可以用一些延迟加载脚本增强它,以支持一些拖放和动画之类的华丽效果。

6.预加载组件

预加载可能看起来和延迟加载是相反的,但它其实有不同的目标。通过预加载组件可以充分利用浏览器空闲的时间来请求将来会用到的组件(图片,样式和脚本)。用户访问下一页的时候,大部分组件都已经在缓存里了,所以在用户看来页面会加载得更快。

实际应用中有以下几种预加载的类型:

无条件预加载——尽快开始加载,获取一些额外的组件。google.com就是一个 sprite 图片预加载的好例子,这个 sprite 图片并不是google.com主页需要的,而是搜索结果页面上的内容。 条件性预加载——根据用户操作猜测用户将要跳转到哪里并据此预加载。在search.yahoo.com的输入框里键入内容后,可以看到那些额外组件是怎样请求加载的。 提前预加载——在推出新设计之前预加载。经常在重新设计之后会听到:“这个新网站不错,但比以前更慢了”,一部分原因是用户访问先前的页面都是有旧缓存的,但新的却是一种空缓存状态下的体验。可以通过在将要推出新设计之前预加载一些组件来减轻这种负面影响,老站可以利用浏览器空闲的时间来请求那些新站需要的图片和脚本。

7.减少 DOM 元素的数量

一个复杂的页面意味着要下载更多的字节,而且用 JavaScript 访问 DOM 也会更慢。举个例子,想要添加一个事件处理器的时候,循环遍历页面上的 500 个 DOM 元素和 5000 个 DOM 元素是有区别的。

大量的 DOM 元素是一种征兆——页面上有些内容无关的标记需要清理。正在用嵌套表格来布局吗?还是为了修复布局问题而添了一堆的<div>或许应该用更好的语义化标记。

YUI CSS utilities对布局有很大帮助:grids.css 针对整体布局,fonts.cssreset.css可以用来去除浏览器的默认格式。这是个开始清理和思考标记的好机会,例如只在语义上有意义的时候使用<div>,而不是因为它能够渲染一个新行。

DOM 元素的数量很容易测试,只需要在 Firebug 的控制台里输入:

document.getElementsByTagName('*').length

那么多少 DOM 元素才算是太多呢?可以参考其它类似的标记良好的页面,例如 Yahoo!主页是一个相当繁忙的页面,但只有不到 700 个元素(HTML 标签)。

8.跨域分离组件

分离组件可以最大化并行下载,但要确保只用不超过 2-4 个域,因为存在 DNS 查找的代价。例如,可以把 HTML 和动态内容部署在www.example.org,而把静态组件分离到 static1.example.orgstatic2.example.org

9.尽量少用iframe

iframe可以把一个 HTML 文档插入到父文档里,重要的是明白 iframe 是如何工作的并高效地使用它。

<iframe>的优点:

  • 引入缓慢的第三方内容,比如标志和广告
  • 安全沙箱
  • 并行下载脚本

<iframe>的缺点:

  • 代价高昂,即使是空白的 iframe
  • 阻塞页面加载
  • 非语义

10.杜绝 404

HTTP 请求代价高昂,完全没有必要用一个 HTTP 请求去获取一个无用的响应(比如 404 Not Found),只会拖慢用户体验而没有任何好处。

有些站点用的是有帮助的 404——“你的意思是 xxx?”,这样做有利于用户体验,,但也浪费了服务器资源(比如数据库等等)。最糟糕的是链接到的外部 JavaScript 有错误而且结果是 404。首先,这种下载将阻塞并行下载。其次,浏览器会试图解析 404 响应体,因为它是 JavaScript 代码,需要找出其中可用的部分。

# css 部分

11.避免使用 CSS 表达式

用 CSS 表达式动态设置 CSS 属性,是一种强大又危险的方式。从 IE5 开始支持,但从 IE8 起就不推荐使用了。例如,可以用 CSS 表达式把背景颜色设置成按小时交替的:

background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );

12.选择<link>舍弃@import

前面提到了一个最佳实践:为了实现逐步渲染,CSS 应该放在顶部。

在 IE 中用@import与在底部用<link>效果一样,所以最好不要用它。

13.避免使用滤镜

IE 专有的 AlphaImageLoader 滤镜可以用来修复 IE7 之前的版本中半透明 PNG 图片的问题。在图片加载过程中,这个滤镜会阻塞渲染,卡住浏览器,还会增加内存消耗而且是被应用到每个元素的,而不是每个图片,所以会存在一大堆问题。

最好的方法是干脆不要用 AlphaImageLoader,而优雅地降级到用在 IE 中支持性很好的 PNG8 图片来代替。如果非要用 AlphaImageLoader,应该用下划线 hack:_filter 来避免影响 IE7 及更高版本的用户。

14.把样式表放在顶部

在 Yahoo!研究性能的时候,我们发现把样式表放到文档的 HEAD 部分能让页面看起来加载地更快。这是因为把样式表放在 head 里能让页面逐步渲染。

关注性能的前端工程师想让页面逐步渲染。也就是说,我们想让浏览器尽快显示已有内容,这在页面上有一大堆内容或者用户网速很慢时显得尤为重要。给用户显示反馈(比如进度指标)的重要性已经被广泛研究过,并且被记录下来了。在我们的例子中,HTML 页面就是进度指标!当浏览器逐渐加载页面头部,导航条,顶部 logo 等等内容的时候,这些都被正在等待页面加载的用户当作反馈,能够提高整体用户体验。

# js 部分

15.去除重复脚本

页面含有重复的脚本文件会影响性能,这可能和你想象的不一样。在对美国前 10 大 web 站点的评审中,发现只有 2 个站点含有重复脚本。两个主要原因增加了在单一页面中出现重复脚本的几率:团队大小和脚本数量。在这种情况下,重复脚本会创建不必要的 HTTP 请求,执行无用的 JavaScript 代码,而影响页面性能。

IE 会产生不必要的 HTTP 请求,而 Firefox 不会。在 IE 中,如果一个不可缓存的外部脚本被页面引入了两次,它会在页面加载时产生两个 HTTP 请求。即使脚本是可缓存的,在用户重新加载页面时也会产生额外的 HTTP 请求。

除了产生没有意义的 HTTP 请求之外,多次对脚本求值也会浪费时间。因为无论脚本是否可缓存,在 Firefox 和 IE 中都会执行冗余的 JavaScript 代码。

避免不小心把相同脚本引入两次的一种方法就是在模版系统中实现脚本管理模块。典型的脚本引入方法就是在 HTML 页面中用 SCRIPT 标签:

<script type="text/javascript" src="menu_1.0.17.js"></script>
1

16.尽量减少 DOM 访问

用 JavaScript 访问 DOM 元素是很慢的,所以,为了让页面反应更迅速,应该:

  • 缓存已访问过的元素的索引
  • 先“离线”更新节点,再把它们添到 DOM 树上
  • 避免用 JavaScript 修复布局问题

17.用智能的事件处理器

有时候感觉页面反映不够灵敏,是因为有太多频繁执行的事件处理器被添加到了 DOM 树的不同元素上,这就是推荐使用事件委托的原因。如果一个 div 里面有 10 个按钮,应该只给div 容器添加一个事件处理器,而不是给每个按钮都添加一个。事件能够冒泡,所以可以捕获事件并得知哪个按钮是事件源。

18.把脚本放在底部

脚本会阻塞并行下载,HTTP/1.1官方文档建议浏览器每个主机名下并行下载的组件数不要超过两个,如果图片来自多个主机名,并行下载的数量就可以超过两个。如果脚本正在下载,浏览器就不开始任何其它下载任务,即使是在不同主机名下的。

有时候,并不容易把脚本移动到底部。举个例子,如果脚本是用 document.write插入到页面内容中的,就没办法再往下移了。还可能存在作用域问题,在多数情况下,这些问题都是可以解决的。

一个常见的建议是用推迟(deferred)脚本,有 DEFER 属性的脚本意味着不能含有document.write,并且提示浏览器告诉他们可以继续渲染。不幸的是,Firefox 不支持 DEFER 属性。在 IE 中,脚本可能被推迟,但不尽如人意。如果脚本可以推迟,我们就可以把它放到页面底部,页面就可以更快地载入。

# javascript, css

19.把 JavaScript 和 CSS 放到外面

很多性能原则都是关于如何管理外部组件的,然而,在这些顾虑出现之前你应该问一个更基础的问题:应该把 JavaScript 和 CSS 放到外部文件中还是直接写在页面里?

实际上,用外部文件可以让页面更快,因为 JavaScript 和 CSS 文件会被缓存在浏览器。HTML 文档中的行内 JavaScript 和 CSS 在每次请求该 HTML 文档的时候都会重新下载。这样做减少了所需的 HTTP 请求数,但增加了 HTML 文档的大小。另一方面,如果 JavaScript 和 CSS 在外部文件中,并且已经被浏览器缓存起来了,那么我们就成功地把 HTML 文档变小了,而且还没有增加 HTTP 请求数。

20.压缩 JavaScript 和 CSS

压缩具体来说就是从代码中去除不必要的字符以减少大小,从而提升加载速度。代码最小化就是去掉所有注释和不必要的空白字符(空格,换行和 tab)。在 JavaScript 中这样做能够提高响应性能,因为要下载的文件变小了。两个最常用的 JavaScript 代码压缩工具是 JSMinYUI CompressorYUI compressor 还可以压缩 CSS。

混淆是一种可选的源码优化措施,要比压缩更复杂,所以混淆过程也更容易产生 bug。在对美国前十的网站调查中,压缩可以缩小 21%,而混淆能缩小 25%。虽然混淆的缩小程度更高,但比压缩风险更大。

除了压缩外部脚本和样式,行内的<script><style>块也可以压缩。即使启用了 gzip模块,先进行压缩也能够缩小 5%或者更多的大小。JavaScriptCSS的用处越来越多,所以压缩代码会有不错的效果。

# 图片

21.优化图片 尝试把 GIF 格式转换成 PNG 格式,看看是否节省空间。在所有的 PNG 图片上运行pngcrush(或者其它 PNG 优化工具)

22.优化 CSS Sprite

  • 在 Sprite 图片中横向排列一般都比纵向排列的最终文件小
  • 组合 Sprite 图片中的相似颜色可以保持低色数,最理想的是 256 色以下 PNG8 格式
  • “对移动端友好”,不要在 Sprite 图片中留下太大的空隙。虽然不会在很大程度上影响图片文件的大小,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100 的图片是 1 万个像素,而1000×1000 的图片就是 100 万个像素了。

23.不要用 HTML 缩放图片

不要因为在 HTML 中可以设置宽高而使用本不需要的大图。如果需要

<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
1

那么图片本身(mycat.jpg)应该是100x100px 的,而不是去缩小500x500px的图片。

24.用小的可缓存的 favicon.ico(P.S. 收藏夹图标)

favicon.ico 是放在服务器根目录的图片,它会带来一堆麻烦,因为即便你不管它,浏览器也会自动请求它,所以最好不要给一个 404 Not Found 响应。而且只要在同一个服务器上,每次请求它时都会发送 cookie,此外这个图片还会干扰下载顺序,例如在 IE 中,当你在 onload 中请求额外组件时,将会先下载favicon

所以为了缓解 favicon.ico的缺点,应该确保:

足够小,最好在 1K 以下 设置合适的有效期 HTTP 头(以后如果想换的话就不能重命名了),把有效期设置为几个月后一般比较安全,可以通过检查当前 favicon.ico的最后修改日期来确保变更能让浏览器知道。

25.给 Cookie 减肥

使用 cookie 的原因有很多,比如授权和个性化。HTTP 头中 cookie 信息在 web 服务器和浏览器之间交换。重要的是保证 cookie 尽可能的小,以最小化对用户响应时间的影响。

  • 清除不必要的 cookie
  • 保证 cookie 尽可能小,以最小化对用户响应时间的影响
  • 注意给 cookie 设置合适的域级别,以免影响其它子域
  • 设置合适的有效期,更早的有效期或者 none 可以更快的删除 cookie,提高用户响应时间

26.把组件放在不含 cookie 的域下

当浏览器发送对静态图像的请求时,cookie 也会一起发送,而服务器根本不需要这些 cookie。所以它们只会造成没有意义的网络通信量,应该确保对静态组件的请求不含 cookie。可以创建一个子域,把所有的静态组件都部署在那儿。

如果域名是 www.example.org,可以把静态组件部署到static.example.org。然而,如果已经在顶级域 example.org或者 www.example.org设置了cookie,那么所有对 static.example.org 的请求都会含有这些 cookie。这时候可以再买一个新域名,把所有的静态组件部署上去,并保持这个新域名不含 cookie。Yahoo!用的是yimg.com,YouTube 是 ytimg.com,Amazon 是images-amazon.com 等等。

把静态组件部署在不含 cookie 的域下还有一个好处是有些代理可能会拒绝缓存带 cookie 的组件。有一点需要注意:如果不知道应该用 example.org 还是 www.example.org 作为主页,可以考虑一下 cookie 的影响。省略 www 的话,就只能把 cookie 写到*.example.org,所以因为性能原因最好用www 子域,并且把 cookie 写到这个子域下。

# 移动端

27.保证所有组件都小于 25K

这个限制是因为 iPhone 不能缓存大于 25K 的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的 gzip 可能不够。

28.把组件打包到一个复合文档里

把各个组件打包成一个像有附件的电子邮件一样的复合文档里,可以用一个 HTTP 请求获取多个组件(记住一点:HTTP 请求是代价高昂的)。用这种方式的时候,要先检查用户代理是否支持(iPhone 就不支持)。

# 服务器

29.Gzip 组件

前端工程师可以想办法明显地缩短通过网络传输 HTTP 请求和响应的时间。毫无疑问,终端用户的带宽速度,网络服务商,对等交换点的距离等等,都是开发团队所无法控制的。但还有别的能够影响响应时间的因素,压缩可以通过减少 HTTP 响应的大小来缩短响应时间。

HTTP/1.1开始,web 客户端就有了支持压缩的 Accept-Encoding HTTP 请求头。

Accept-Encoding: gzip, deflate

如果 web 服务器看到这个请求头,它就会用客户端列出的一种方式来压缩响应。web 服务器通过 Content-Encoding相应头来通知客户端。

Content-Encoding: gzip

尽可能多地用 gzip 压缩能够给页面减肥,这也是提升用户体验最简单的方法。

30.避免图片 src 属性为空

Image with empty string src 属性是空字符串的图片很常见,主要以两种形式出现:

①straight HTML:

<img src="””" />
1

②JavaScript

var img = new Image();
img.src = “”;
1
2

这两种形式都会引起相同的问题:浏览器会向服务器发送另一个请求。

31.配置 ETags

实体标签(ETags),是服务器和浏览器用来决定浏览器缓存中组件与源服务器中的组件是否匹配的一种机制(“实体”也就是组件:图片,脚本,样式表等等)。添加 ETags 可以提供一种实体验证机制,比最后修改日期更加灵活。一个 ETag 是一个字符串,作为一个组件某一具体版本的唯一标识符。唯一的格式约束是字符串必须用引号括起来,源服务器用相应头中的 ETag 来指定组件的 ETag:

HTTP/1.1 200 OK
      Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
      ETag: "10c24bc-4ab-457e1c1f"
      Content-Length: 12195
1
2
3
4

然后,如果浏览器必须验证一个组件,它用If-None-Match 请求头来把ETag传回源服务器。如果 ETags匹配成功,会返回一个 304 状态码,这样就减少了12195 个字节的响应体。

GET /i/yahoo.gif HTTP/1.1
Host: us.yimg.com
If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT
If-None-Match: "10c24bc-4ab-457e1c1f"
HTTP/1.1 304 Not Modified
1
2
3
4
5

32.对 Ajax 用 GET 请求 Yahoo!邮箱团队发现使用XMLHttpRequest 时,浏览器的 POST 请求是通过一个两步的过程来实现的:先发送 HTTP 头,在发送数据。所以最好用 GET 请求,它只需要发送一个 TCP 报文(除非 cookie 特别多)。IE 的 URL 长度最大值是 2K,所以如果要发送的数据超过 2K 就无法使用 GET 了。

POST 请求的一个有趣的副作用是实际上没有发送任何数据,就像 GET 请求一样。正如 HTTP 说明文档中描述的,GET 请求是用来检索信息的。所以它的语义只是用 GET 请求来请求数据,而不是用来发送需要存储到服务器的数据。

33.尽早清空缓冲区 当用户请求一个页面时,服务器需要用大约 200 到 500 毫秒来组装 HTML 页面,在这期间,浏览器闲等着数据到达。PHP 中有一个flush()函数,允许给浏览器发送一部分已经准备完毕的 HTML 响应,以便浏览器可以在后台准备剩余部分的同时开始获取组件,好处主要体现在很忙的后台或者很“轻”的前端页面上(也就是说,响应时耗主要在后台方面时最能体现优势)。

较理想的清空缓冲区的位置是 HEAD 后面,因为 HTML 的 HEAD 部分通常更容易生成,并且允许引入任何 CSS 和 JavaScript 文件,这样就可以让浏览器在后台还在处理的时候就开始并行获取组件。

例如:

... <!-- css, js -->

</head>
<?php flush(); ?>
<body>
...<!-- content -->
1
2
3
4
5
6

34.使用 CDN(内容分发网络)

用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢?

实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的 web 应用程序来适应分布式结构。这取决于应用程序,改变结构可能包括一些让人望而生畏的任务,比如同步会话状态和跨服务器复制数据库事务(翻译可能不准确)。缩短用户和内容之间距离的提议可能被推迟,或者根本不可能通过,就是因为这个难题。

记住终端用户 80%到 90%的响应时间都花在下载页面组件上了:图片,样式,脚本,Flash 等等,这是业绩黄金法则。最好先分散静态内容,而不是一开始就重新设计应用程序结构。这不仅能够大大减少响应时间,还更容易表现出 CDN 的功劳。

内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。

35.添上 Expires 或者 Cache-Control HTTP 头

这条规则有两个方面:

  • 对于静态组件:通过设置一个遥远的将来时间作为 Expires 来实现永不失效
  • 多余动态组件:用合适的 Cache-ControlHTTP头来让浏览器进行条件性的请求   网页设计越来越丰富,这意味着页面里有更多的脚本,图片和 Flash。站点的新访客可能还是不得不提交几个 HTTP 请求,但通过使用有效期能让组件变得可缓存,这避免了在接下来的浏览过程中不必要的 HTTP 请求。有效期 HTTP 头通常被用在图片上,但它们应该用在所有组件上,包括脚本、样式和 Flash 组件。

浏览器(和代理)用缓存来减少 HTTP 请求的数目和大小,让页面能够更快加载。web 服务器通过有效期 HTTP 响应头来告诉客户端,页面的各个组件应该被缓存多久。用一个遥远的将来时间做有效期,告诉浏览器这个响应在 2010 年 4 月 15 日前不会改变。

Expires: Thu, 15 Apr 2010 20:00:00 GMT

如果你用的是 Apache 服务器,用 ExpiresDefault 指令来设置相对于当前日期的有效期。下面的例子设置了从请求时间起 10 年的有效期:

ExpiresDefault "access plus 10 years"

# 参考

[1]别再说那些老掉牙的性能优化了 (opens new window)

[2]虎前端优化的35条军规 (opens new window)