网站性能优化篇(一)

前言

1) 浏览器渲染页面的过程

从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:

  • 1.1 DNS 查询
  • 1.2 TCP 连接
  • 1.3 HTTP 请求即响应
  • 1.4 服务器响应
  • 1.5 客户端渲染

下面我主要从客户端渲染的角度来详细解析:

  • 处理 HTML 标记并构建 DOM 树。
  • 处理 CSS 标记并构建 CSSOM 树。
  • 将 DOM 与 CSSOM 合并成一个渲染树。
  • 根据渲染树来布局,以计算每个节点的几何信息。
  • 将各个节点绘制到屏幕上。

需要明白,这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM,下面就来看看它们的影响方式。

阻塞渲染:CSS 与 JavaScript

默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至CSSOM构建完毕。JavaScript 不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。

存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。另外:当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。JavaScript 可以查询和修改 DOM 与 CSSOM。CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。

所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:

CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。

JavaScript 应尽量少影响 DOM 的构建。

浏览器的发展日益加快(目前的 Chrome 官方稳定版是 61),具体的渲染策略会不断进化,但了解这些原理后,就能想通它进化的逻辑。下面来看看 CSS 与 JavaScript 具体会怎样阻塞资源。

css

1
2
<style> p { color: red; }</style>
<link rel="stylesheet" href="index.css">

这样的 link 标签(无论是否 inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至 CSSOM 构建完毕。

渲染树(Render-Tree)的关键渲染路径中,要求同时具有 DOM 和 CSSOM,之后才会构建渲染树。即,HTML 和 CSS 都是阻塞渲染的资源。HTML 显然是必需的,因为包括我们希望显示的文本在内的内容,都在 DOM 中存放,那么可以从 CSS 上想办法。

最容易想到的当然是精简 CSS 并尽快提供它。除此之外,还可以用媒体类型(media type)和媒体查询(media query)来解除对渲染的阻塞。

1
2
3
<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

第一个资源会加载并阻塞。
第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。
第三个资源提供了媒体查询,会在符合条件时阻塞渲染。

JavaScript

JavaScript 的情况比 CSS 要更复杂一些。观察下面的代码:

1
2
3
4
5
6
7
8
9
10
11
<p>Do not go gentle into that good night,</p>
<script>console.log("inline")</script>
<p>Old age should burn and rave at close of day;</p>
<script src="app.js"></script>
<p>Rage, rage against the dying of the light.</p>

<p>Do not go gentle into that good night,</p>
<script src="app.js"></script>
<p>Old age should burn and rave at close of day;</p>
<script>console.log("inline")</script>
<p>Rage, rage against the dying of the light.</p>

这样的 script 标签会阻塞 HTML 解析,无论是不是 inline-script。上面的 P 标签会从上到下解析,这个过程会被两段 JavaScript 分别打断一次(加载并且执行的时间段内)。

所以实际工程中,我们常常将资源放到文档底部

**改变阻塞模式:deferasync **

为什么要将 script 加载的 defer 与 async 方式放到后面呢?因为这两种方式是的出现,全是由于前面讲的那些阻塞条件的存在。换句话说,defer 与 async 方式可以改变之前的那些阻塞情形。

首先,注意 async 与 defer 属性对于 inline-script 都是无效的,所以下面这个示例中三个 script 标签的代码会从上到下依次执行。

1
2
3
4
5
6
7
8
9
10
<!-- 按照从上到下的顺序输出 1 2 3 -->
<script async>
console.log("1");
</script>
<script defer>
console.log("2");
</script>
<script>
console.log("3");
</script>

故,下面两节讨论的内容都是针对设置了 src 属性的 script 标签。

** defer **

1
2
3
<script src="app1.js" defer></script>
<script src="app2.js" defer></script>
<script src="app3.js" defer></script>

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 不会改变 script 中代码的执行顺序,示例代码会按照 1、2、3 的顺序执行。所以,defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。

** async **

1
2
3
<script src="app.js" async></script>
<script src="ad.js" async></script>
<script src="statistics.js" async></script>

** document.createElement **

使用 document.createElement 创建的 script 默认是异步的,示例如下。

1
console.log(document.createElement("script").async); // true

所以,通过动态添加 script 标签引入 JavaScript 文件默认是不会阻塞页面的。如果想同步执行,需要将 async 属性人为设置为 false。

如果使用 document.createElement 创建 link 标签会怎样呢?

1
2
3
4
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = "index.css";
document.head.appendChild(style); // 阻塞?

document.write 与 innerHTML

通过 document.write 添加的 link 或 script 标签都相当于添加在 document 中的标签,因为它操作的是 document stream(所以对于 loaded 状态的页面使用 document.write 会自动调用document.open,这会覆盖原有文档内容)。即正常情况下, link 会阻塞渲染,script 会同步执行。不过这是不推荐的方式,Chrome 已经会显示警告,提示未来有可能禁止这样引入。如果给这种方式引入的 script 添加 async 属性,Chrome 会检查是否同源,对于非同源的 async-script 是不允许这么引入的。

如果使用 innerHTML 引入 script 标签,其中的 JavaScript 不会执行。当然,可以通过 eval() 来手工处理,不过不推荐。如果引入 link 标签,我试验过在 Chrome 中是可以起作用的。另外,outerHTMLinsertAdjacentHTML()应该也是相同的行为,我并没有试验。这三者应该用于文本的操作,即只使用它们添加 text 或普通 HTMLElement

so ,我们来看看雅虎工程师的总结

2)前端资源优化

2.1 图片资源压缩(tinypng )

图片无损压缩神站- tinypng.com

TinyPNG使用智能有损压缩技术来减少PNG文件的文件大小。 通过有选择地减少图像中的颜色数量,需要更少的字节来存储数据。 效果几乎看不见,但是它在文件大小上造成了很大的差异!

PNG很有用,因为它是可以存储部分透明图像的唯一广泛支持的格式。 格式使用压缩,但文件可能仍然很大。 使用TinyPNG缩小应用程序和网站的图像。 它将更快地使用更少的带宽和负载。

tinypng.com

2.2 使用雪碧图(CSS Sprite)

雪碧图的概念大家一定在开发中经常听见,其实雪碧图是减小请求数的示范性代表。而且很奇妙的是,多张图片拼在一块后,总体积会比之前所有图片的体积之和小(你可以亲自试试)。这里给大家推荐一个自动化生成雪碧图的工具:https://www.toptal.com/developers/css/sprite-generator。只要你添加相关资源文件,他就会自动帮你生成雪碧图以及对应的 CSS 样式,你要做的,只是 downloadcopy

演示

2.3 使用字体图标(iconfont)

网址:www.iconfont.cn/
不论是压缩后的图片,还是雪碧图,终归还是图片,只要是图片,就还是会占用大量网络传输资源。但是字体图标的出现,却让前端开发者看到了另外一个神奇的世界。我最喜欢用的是阿里矢量图标库 ,里面有大量的矢量图资源,而且你只需要像在淘宝采购一样把他们添加至购物车就能把它们带回家,整理完资源后还能自动生成CDN链接,可以说是完美的一条龙服务了。

iconfont

2.4 使用 CDN (BootCDN)

网址:www.bootcdn.cn
为什么一定要使用 CDN
简单来讲,使用 CDN 可以让你的网站中引用的一些常见的资源(css & js)加载的更快,更稳定。尤其是对于网站中使用了大量的静态资源,效果更加明显。

CDN 介绍参考这里

2.4 图片懒加载(jQuery_lazyload)

网址:https://github.com/tuupola/jquery_lazyload

通过给网站中用到的图片设置懒加载,让不在可视区域的图片延时加载,可以加快网页加载的速度。这对网站优化显得非常重要!

2.5 小图转 Base64

通过把小图转换成 base64 格式,直接写到 HTML 中。可以减少网络请求!

3)服务器性能优化

3.1.浏览器缓存

我们都知道,浏览器在向服务器发起请求前,会先查询本地是否有相同的文件,如果有,就会直接拉取本地缓存,如果没有才会发起资源请求。

浏览器默认的缓存是放在内存内的,但我们知道,内存里的缓存会因为进程的结束或者说浏览器的关闭而被清除,而存在硬盘里的缓存才能够被长期保留下去。很多时候,我们在network面板中各请求的size项里,会看到两种不同的状态:from memory cache 和 from disk cache,前者指缓存来自内存,后者指缓存来自硬盘。而控制缓存存放位置的,不是别人,就是我们在服务器上设置的Etag字段。在浏览器接收到服务器响应后,会检测响应头部(Header),如果有Etag字段,那么浏览器就会将本次缓存写入硬盘中。之所以拉取缓存会出现200、304两种不同的状态码,取决于浏览器是否有向服务器发起验证请求。

只有向服务器发起验证请求并确认缓存未被更新,才会返回304状态码。这里我以nginx为例,谈谈如何配置缓存:

首先,我们先进入nginx的配置文档

1
$ vim nginxPath/conf/nginx.conf

在配置文档内插入如下两项:

1
2
3
etag on;   //开启etag验证
expires 7d; //设置缓存过期时间为7天

打开我们的网站,在chrome devtools的network面板中观察我们的请求资源,如果在响应头部看见Etag和Expires字段,就说明我们的缓存配置成功了。

缓存配置

3.2 资源打包压缩

我们之前所作的浏览器缓存工作,只有在用户第二次访问我们的页面才能起到效果,如果要在用户首次打开页面就实现优良的性能,必须对资源进行优化。我们常将网络性能优化措施归结为三大方面:减少请求数、减小请求资源体积、提升网络传输速率。现在,让我们逐个击破:结合前端工程化思想,我们在对上线文件进行自动化打包编译时,通常都需要打包工具的协助,这里我推荐webpack,我通常都使用GulpGrunt来编译。

我的网站性能监测

网站性能监测