前端性能优化

摘要:前端面试几乎都会问这样一个问题:说说你对前端性能优化的了解。这个问题相当于是开卷考试了,因为也没啥标准答案。但是还是有一些常见套路的,这里就尝试着罗列一些,希望能总结越多,理解越多。

1. 编码优化

1.1 提升数据读取速度

  • 字面量与局部变量的访问速度最快,数组元素和对象成员相对较慢。
  • 变量从局部作用域到全局作用域的搜索过程越长速度越慢。
  • 对象嵌套的越深,读取速度就越慢。
  • 对象在原型链中存在的位置越深,找到它的速度就越慢。

1.2 减少 DOM 数量和操作

  • 在JS中对DOM进行访问的代价非常高。请尽可能减少访问DOM的次数。建议缓存DOM属性和元素、把DOM集合的长度缓存到变量中并在迭代中使用。读变量比读DOM的速度要快很多。
  • 重排与重绘的代价非常昂贵。如果操作需要进行多次重排与重绘,建议先让元素脱离文档流,处理完毕后再让元素回归文档流,这样浏览器只会进行两次重排与重绘(脱离时和回归时)。
  • 善于使用事件委托。

1.3 减少 CSS 表达式

CSS 表达式的问题在于它被重新计算的次数远比想象的要多,不仅在网页绘制或大小改变时计算,即使滚动屏幕或者移动鼠标的时候也在计算,所以尽量避免使用它来防止使用不当而造成的性能损耗。如果想达到类似的效果可以通过简单的 JavaScript 做到。

2. 静态资源优化

2.1 减少 http 请求次数

  • 捆绑压缩文件: 将多个脚本或样式文件打包并压缩成一个文件。
  • CSS Sprites:将多张图片拼成一张图片,由 CSS 控制局部图片的显示。
  • base64 转换:将较小的图片通过 base64 转换为字符串以减少 http 请求。
  • 不要通过缩放图片来适应页面。如果需要小图片,就直接使用小图片。不要加载大图片再缩放成小图片。
  • 使用 GET Ajax 请求:浏览器在实现 XMLHttpRequest POST 的时候分成两步,先发 header,然后发送数据。而 GET 却可以用一个 TCP 报文完成请求,所以使用 Ajax 请求数据时尽量通过 GET 来完成。
  • 使用小且可缓存的 favicon.ico:网站图标文件 favicon.ico,不管服务器有还是没有,浏览器都会去尝试请求这个图标。所以要确保这个图标存在。文件要尽量小,最好小于 1k,同时设置一个长的过期时间。

Cookie 被用来做认证或个性化设置,其信息被包含在 http 报文头中。常见的技巧有:

  • 去除没有必要的 Cookie,如果网页不需要 Cookie 就完全禁掉。
  • 将 Cookie 的大小减到最小。
  • 注意 Cookie 设置的 domain 级别,没有必要情况下不要影响到 sub-domain。
  • 设置合适的过期时间,比较长的过期时间可以提高响应速度。

2.3 采取适当的资源引用方式

使用外链的 JavaScript 和 CSS 文件可以使这些文件被浏览器缓存,从而在不同的请求内容之间重用,同时也减小了网页内容的大小。

不过,使用外链的决定因素在于这些外部文件的重用率,如果用户在浏览网站时会访问多次相同页面或者可以重用脚本的不同页面,那么外链形式可以带来很大的好处。但对于用户通常只会访问一次的页面,那么 inline 的方式相对来说可以提供更高的效率。

2.4 异步无阻塞加载 JS

JS 的加载与执行会阻塞页面渲染,可以将 script 标签放到页面的最底部。但是更好的做法是异步无阻塞加载 JS。

有多种无阻塞加载JS的方法:defer、async、动态创建 script 标签、使用 XHR 异步请求 JS 代码并注入到页面。

但更推荐的做法是使用 defer 或 async。如果使用 defer 或 async 请将 script 标签放到 head 标签中,以便让浏览器更早地发现资源并在后台线程中解析并开始加载 JS。

2.5 优先加载关键的 CSS

CSS 资源的加载对浏览器渲染的影响很大,默认情况下浏览器只有在完成 head 标签中 CSS 的加载与解析之后才会渲染页面。

如果 CSS 文件过大,用户就需要等待很长的时间才能看到渲染结果。

针对这种情况可以将首屏渲染必须用到的 CSS 提取出来内嵌到 head 中,然后再将剩余部分的 CSS 用异步的方式加载。

3. 交付优化

3.1 服务器端设置

  • Gzip 压缩传输文件:Gzip 通常可以减少网页内容 70% 的大小,包括脚本、样式表、图片等文件,主流服务器都有相应的压缩支持模块。
  • 使用 CDN:前端性能优化的第一定律就是减少网页内容的下载时间。CDN 就可以有效地提升下载速度。
  • 减少 DNS 查询次数:DNS 查询也会消耗响应时间。如果网页内容来自多个不同的 domain,那么浏览器首次解析这些 domain 就需要消耗一定的时间。所以应该尽可能少地使用外部资源或保持外部域名的少样性。
  • 对静态内容添加 Expires,可以将静态内容设为永不过期,或者很长时间以后。
  • 对动态内容应用合适的 Cache-Control,可以让浏览器根据条件来发送请求并使响应可以被浏览器缓存。

大多数网站的静态资源都没必要设置 cookie,所以可以采用不同的 domain 来单独存放这些静态文件,这样做不仅可以减少 Cookie 大小从而提高响应速度,还有一个好处是有些 proxy 拒绝缓存带有 Cookie 的内容,如果能将这些静态资源的 Cookie 去除,那就可以得到这些 proxy 的缓存支持。一般网站会将静态资源放在类似于 static.example.com 或 CDN 上,动态内容放在 www.example.com 上。

浏览器一般对同一个域的下载连接数有所限制,按照域名划分下载内容可以增大并行下载连接数。但是注意控制域名数量在 2-4 个之间,不然 DNS 查询也是个问题。

4. 构建优化

4.1 延迟加载、提前加载

延迟加载是基于我们知道网页最初加载需要的最小内容集是什么,剩下的内容就可以加入延迟加载的集合中。JavaScript 是典型的可以延迟加载的内容。

提前加载则是为了提前加载接下来网页中访问的资源。

4.2 使用 Tree-shaking、Scope hoisting、Code-splitting

Tree-shaking是一种在构建过程中清除无用代码的技术。使用Tree-shaking可以减少构建后文件的体积。

目前Webpack与Rollup都支持Scope Hoisting。它们可以检查import链,并尽可能的将散乱的模块放到一个函数中,前提是不能造成代码冗余。所以只有被引用了一次的模块才会被合并。使用Scope Hoisting可以让代码体积更小并且可以降低代码在运行时的内存开销,同时它的运行速度更快。前面2.1节介绍了变量从局部作用域到全局作用域的搜索过程越长执行速度越慢,Scope Hoisting可以减少搜索时间。

code-splitting是Webpack中最引人注目的特性之一。此特性能够把代码分离到不同的bundle中,然后可以按需加载或并行加载这些文件。code-splitting可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

4.3 服务端渲染(SSR)

服务端渲染(Server Side Render,简称SSR)的意义在于弥补主要内容在前端渲染的成本,减少白屏时间,提升首次有效绘制的速度。可以使用服务端渲染来获得更快的首次有效绘制。

4.4 使用 import 函数动态导入模块

使用import函数可以在运行时动态地加载ES2015模块,从而实现按需加载的需求。

这种优化在单页应用中变得尤为重要,在切换路由的时候动态导入当前路由所需的模块,会避免加载冗余的模块。

试想如果在首次加载页面时一次性把整个站点所需要的所有模块都同时加载下来会加载多少非必须的JS,应该尽可能的让加载的JS更小,只在首屏加载需要的JS。