跳到主要内容

前端性能优化

· 19 分钟阅读

优化目标:

  • 响应时间控制在 100ms,帧速控制在 60帧/秒
  • 速度指标(SpeedIndex-是显示页面可见部分的平均时间) < 1250,3G上的交互时间(Interaction time)小于5s,关键文件大小预算 < 170 Kb(gzip)
  • 首先加载核心体验,然后是增强功能,最后才是附加功能。

静态资源

图像优化

  • WebP
  • 图片压缩神器
  • 使用字体代替icon
  • 延迟加载 合理使用图片尺寸,如果用到小图片,就使用相应大小的图片,而不是将200x200的缩小为100x100。否则这其中就有(200x200)-(100x100)=30000个像素是浪费的,这占到了图片尺寸的75%!
  • <img width="100" height="100" src="hahaha.jpg" /> 如果可能的话,设置图片的宽和高,以免浏览器按照「猜」的宽高给图片保留的区域和实际宽高差异,产生重绘。
  • 考虑使用video循环替代GIF--权衡

    浏览器不会预加载 video 内容,但 video 往往要比gif更轻更小

  • 添加渐进式图片加载来将延迟加载提升到新的水平。 与 Facebook,Pinterest 和 Medium 类似,你可以先加载低质量或模糊的图像,然后当页面继续加载时,使用LQIP (Low Quality Image Placeholders) technique(低质量图像占位符技术),替换它们的清晰版本

文件大小优化

  • 客户端:js、css压缩(webpack/gulp)
  • 服务端:gzip编码

资源加载优化

  • CSS 文件放在<head>中,先外链,后本页
  • script标签放在<body>底部。因为 JS 文件执行会阻塞渲染。
    • 当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。
    • 对于没有任何依赖的 JS 文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。
  • 使用资源预加载preload和资源预读取prefetchMDN
    • preload让浏览器提前加载指定资源,需要执行时再执行,可以加速本页面的加载速度
    • prefetch告诉浏览器加载下一页面可能会用到的资源,可以加速下一个页面的加载速度

减少 HTTP 请求个数

  • JS/CSS:合并JS/CSS文件,gulp/webpack

    如果不进行文件合并,有如下3个隐患:文件与文件之间有插入的上行请求,增加了N-1个网络延迟/受丢包问题影响更严重/经过代理服务器时可能会被断开 但是文件合并本身也有自己的问题: 首屏渲染问题/缓存失效问题 最优方案: 公共库合并/不同页面单独合并/按需加载

  • 图片合并:使用CSS Sprite将背景图片合并成一个文件,通过background-image 和 background-position 控制显示
  • 图片优化:base64(行内图片)
  • 延迟加载:非页面首次加载的内容都可以延时加载:非首屏使用的数据、样式、脚本、图片等,用户交互时才会显示的内容(如抽屉、modal窗口展示)
  • 不使用CSS @import

合理应用缓存

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略

  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存,如spa应用的html文件。
  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。
  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。

浏览器缓存策略

第一次请求
第一次请求

之后的请求
再次请求

避免使用空的src和href

  • a标签设置空的href,非必要会重定向到当前的页面地址
  • form设置空的method,会提交多余表单到当前的页面地址
  • img标签src属性为空字符串,但浏览器仍然会向服务器发起一个HTTP请求(404)

Service Worker

Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)demo

代码性能

代码执行速度影响界面展示优化

代码懒执行

某些逻辑在使用时再进行展示/计算(如图片,js组件模块)。可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要触发,可以通过定时器或者事件的调用/异步来唤醒。

DOM优化

  • 缓存已经访问过的DOM const div = document.getElementById('oDiv') 由于查询DOM比较耗时,在同一个节点无需多次查询的情况下,可以缓存DOM
  • 减少DOM深度及DOM数量 HTML 中标签元素越多,标签的层级越深,浏览器解析DOM并绘制到浏览器中所花的时间就越长,所以应尽可能保持 DOM 元素简洁和层级较少。
  • 批量操作DOM 由于DOM操作比较耗时,且可能会造成回流,因此要避免频繁操作DOM,可以批量操作DOM,先用字符串拼接完毕,再用innerHTML更新DOM
  • 批量操作CSS样式 通过切换class或者使用元素的style.csstext属性去批量操作元素样式
  • 在内存中操作DOM 使用DocumentFragment对象,让DOM操作发生在内存中,而不是页面上,不是append append append...
  • DOM元素离线更新 对DOM进行相关操作时,例、appendChild等都可以使用Document Fragment对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用display:none 对元素隐藏,在元素“消失”后进行相关操作
  • DOM读写分离 浏览器具有惰性渲染机制,连接多次修改DOM可能只触发浏览器的一次渲染。而如果修改DOM后,立即读取DOM。为了保证读取到正确的DOM值,会触发浏览器的一次渲染。因此,修改DOM的操作要与访问DOM分开进行
  • 事件代理 事件代理是指将事件监听器注册在父级元素上,由于子元素的事件会通过事件冒泡的方式向上传播到父节点,因此,可以由父节点的监听函数统一处理多个子元素的事件

    利用事件代理,可以减少内存使用,提高性能及降低代码复杂度

  • 能通过伪元素实现的功能,就没必要添加额外元素,如清除浮动
  • 减少使用table布局(会产生大量重绘)

JavaScript

  • 移除不必要的js代码,如lodash优化(babel-loader & babel-plugin-lodash) 参考

  • 防抖和节流 使用函数节流(throttle)或函数防抖(debounce),限制某一个方法的频繁触发

  • 及时清理环境 及时消除对象引用,清除定时器,清除事件监听器,创建最小作用域变量,可以及时回收内存

  • 尽早处理事件,在DOMContentLoaded即可进行,不用等到load以后

    Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。 DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

  • 使用requestAnimationFrame来替代setTimeout和setInterval

    使用 setTimeout 或者 setInterval 来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而导致该帧后面需要进行的事情没有完成,引发丢帧

  • 使用IntersectionObserver API来实现图片可视区域的懒加载

    Intersection Observer API 会注册一个回调方法,每当期望被监视的元素进入或者退出另外一个元素的时候(或者浏览器的视口)该回调方法将会被执行,或者两个元素的交集部分大小发生变化的时候回调方法也会被执行。实现方案

CSS

  • 使用硬件加速,如translate3D代替translateX/Y/Z
  • 避免选择器嵌套过深(安利scss@at-root{}) 性能排序
    • id选择器(#myid)
    • 类选择器(.myclassname)
    • 标签选择器(div,h1,p)
    • 相邻选择器(h1+p)
    • 子选择器(ul > li)
    • 后代选择器(li a)
    • 通配符选择器(*)
    • 属性选择器(a[rel="external"]
    • 伪类选择器(a:hover,li:nth-child)
  • 减少重绘回流

构建工具优化(webpack/gulp)

  • CommonsChunkPlugin提取公共代码
  • Tree-shaking无用代码移除。通过只加载生产中实际使用的代码并清除在 Webpack 中 未使用的 import
  • Scope hoisting作用域提升(了解作用)。 webpack默认代码会将将所有模块都用函数包裹起来,然后自己实现了一套模块加载、执行与缓存的功能,使用这样的结构是为了更容易实现 Code Splitting(包括 按需加载)、模块热替换等功能。

    想象一下模块导入一个方法foo的情况,该方法需要 webpack_require 从C模块导入另一个B模块,B模块再引入A模块... 使用 scope hoisting 后会把需要导入的文件直接移入导入模块顶部

    • 代码量明显减少
    • 减少多个函数后内存占用减少
    • 不用多次使用 webpack_require 调用模块,运行速度也会得到提升
  • Code-splitting 可将代码分解为按需加载的块。并不是所有的 JavaScript 都是必须下载、解析和编译的。
  • preload-webpack-plugin(参考)。获取代码拆分的路径,然后使用 <link rel="preload"> 或者 <link rel="prefetch"> 提示浏览器预加载它们
  • babel差异化处理,只转换现代浏览器不支持的 ES2015+ 的特性。然后设置两个构建,一个为 ES6 一个为 ES5,这两个包都是只提供给实际需要它们的传统浏览器(参考)。
  • 优化依赖模块的大小,如:lodash优化(babel-loader & babel-plugin-lodash) 参考
  • 按需加载,Webpack 内置了对 import(*) 语句的支持,import 返回一个 Promise,当文件加载成功时可以在 Promise 的 then 方法中获取到 show.js 导出的内容
    window.document.getElementById('btn').addEventListener('click', function () {
    // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
    import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
    })
    });

重绘(Repaint)和回流(Reflow)

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。

    回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。

导致性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

重绘和回流其实和 Event loop 有关

  1. 当 Event loop 执行完 Microtasks(微任务) 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面
  9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

减少重绘和回流(参考)

  1. 使用 translate 替代 top
  2. 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  3. 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来
  4. 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
  5. 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  6. 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  7. CSS 选择符从右往左匹配查找,避免 DOM 深度过深(scss多使用@root{})
  8. 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

Webworker

当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。用户可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。 执行 JS 代码过长会卡住渲染,对于需要很多时间计算的代码可以考虑使用 Webworker。Webworker 可以让我们另开一个线程执行脚本而不影响渲染。

请注意,Web Worker 不能访问 DOM,因为 DOM 不是一个 安全线程,并且执行的代码需要包含在一个单独的文件中。

面向服务端

代码部署后,客户端与服务器通信优化

HTTP/2

因为浏览器会有并发请求限制,在 HTTP / 1.0 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间。

在 HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小。

  • 服务器推送关键CSS,比如,浏览器只请求了index.html,但是服务器把index.html、style.css、example.png全部发送给浏览器。这样的话,只需要一轮 HTTP 通信,浏览器就得到了全部资源,提高了性能。阮一峰
  • 启用 HPACK压缩 (HTTP2 头部压缩算法)
  • 启用 OCSP stapling。可以加快 TLS 握手速度。在线证书状态协议(OCSP)作为证书撤销列表(CRL)协议的替代方案。两种协议都用于检查 SSL 证书是否已被撤销。但是,OCSP 协议不要求浏览器花时间下载然后在列表中搜索证书信息,因此减少握手所需要的时间。

CDN

CDN全称是Content Delivery Network,即内容分发网络,它能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度

静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名

CDN执行过程

  1. 首先访问本地的 DNS ,如果没有命中,继续递归或者迭代查找,直到命中拿到对应的 IP 地址。
  2. 拿到对应的 IP 地址之后服务器端发送请求到目的地址。注意这里返回的不直接是 cdn 服务器的 IP 地址,而是全局负载均衡系统的 IP 地址
  3. 全局负载均衡系统会根据客户端的 IP地址和请求的 url 和相应的区域负载均衡系统通信
  4. 区域负载均衡系统拿着这两个东西获取距离客户端最近且有相应资源的cdn 缓存服务器的地址,返回给全局负载均衡系统
  5. 全局负载均衡系统返回确定的 cdn 缓存服务器的地址给客户端。
  6. 客户端请求缓存服务器上的文件

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。 (面试题,如何判断某台服务器的资源挂掉---通过查询xff找到目标服务器)

使用CDN好处?(面向前端的CDN原理介绍)

  • 提升网页加载速度
  • 处理高流量负载
  • 无需?本完成本地化覆盖
  • 减少带宽消耗
  • 在多台服务器间均衡负载
  • 使你的网站免于DDoS(拒绝服务)的攻击
  • ……

cookie方向

  • 设置 Cookie 的 domain 级别,如无必要,不要影响到 sub-domain
  • 去除不必要的 Cookie,尽量压缩 Cookie 大小
  • 如果仅仅是前端使用的缓存数据,尽量使用sessionStorage/localStorage
  • 注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie

Ajax

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

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

  • 多次使用的接口数据,考虑缓存到内存中(vuex、redux等最佳实践)

DNS 预解析

DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。 <link rel="dns-prefetch" href="//163.com" />

DNS解析过程

  1. 浏览器中输入想要访问的网站的域名,操作系统会先检查本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
  2. 如果hosts里没有这个域名的映射,客户端会向本地DNS服务器发起查询。本地DNS服务器收到查询时,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析。
  3. 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置,采用递归或者迭代查询,直至解析完成。

    当浏览器访问一个域名的时候,需要解析一次DNS,获得对应域名的ip地址。在解析过程中,按照浏览器缓存、系统缓存、路由器缓存、ISP(运营商)DNS缓存、根域名服务器、顶级域名服务器、主域名服务器的顺序,逐步读取缓存,直到拿到IP地址

减少重定向

尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载,降低了用户体验。

如果一定要使用重定向,如http重定向到https,要使用301永久重定向,而不是302临时重定向。因为,如果使用302,则每一次访问http,都会被重定向到https的页面。而永久重定向,在第一次从http重定向到https之后 ,每次访问http,会直接返回https的页面

雅虎军规

雅虎军规35条

  1. 尽量减少 HTTP 请求个数——须权衡
  2. 使用 CDN(内容分发网络)
  3. 为文件头指定 Expires 或 Cache-Control ,使内容具有缓存性。
  4. 避免空的 src 和 href
  5. 使用 gzip 压缩内容
  6. 把 CSS 放到顶部
  7. 把 JS 放到底部
  8. 避免使用 CSS 表达式
  9. 将 CSS 和 JS 放到外部文件中
  10. 减少 DNS 查找次数
  11. 精简 CSS 和 JS
  12. 避免跳转
  13. 剔除重复的 JS 和 CSS
  14. 配置 ETags
  15. 使 AJAX 可缓存
  16. 尽早刷新输出缓冲
  17. 使用 GET 来完成 AJAX 请求
  18. 延迟加载
  19. 预加载
  20. 减少 DOM 元素个数
  21. 根据域名划分页面内容
  22. 尽量减少 iframe 的个数
  23. 避免 404
  24. 减少 Cookie 的大小
  25. 使用无 cookie 的域
  26. 减少 DOM 访问
  27. 开发智能事件处理程序
  28. <link> 代替 @import
  29. 避免使用滤镜
  30. 优化图像
  31. 优化 CSS Spirite
  32. 不要在 HTML 中缩放图像——须权衡
  33. favicon.ico要小而且可缓存
  34. 保持单个内容小于25K
  35. 打包组件成复合文本

性能优化指标1

参考1
参考2
参考3
参考4
参考5