讲清楚说明白 - 渲染性能

535 阅读19分钟

为什么操作 DOM 慢

  • DOM和JS两个线程之间的通信
  • 操作 DOM 可能还会带来重绘回流的情况
  • 插入几万个 DOM,如何实现页面不卡顿
    • requestAnimationFrame 的方式去循环的插入 DOM
    • 虚拟滚动

requestAnimationFrame

  • createDocumentFragment

    文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流

  • requestAnimationFrame

    要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

    • cancelAnimationFrame 取消回调函数请求
    • 回调函数执行次数通常与浏览器屏幕刷新次数相匹配
    • 为了提高性能和电池寿命,在大多数浏览器里,当 requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用
var sum = 30000;
var step = 2;
var inertCount = sum / step;
var start = 0;
function render() {
  var fragment = document.createDocumentFragment();
  for (var i = 0; i < step; i++) {
    var li = document.createElement("li");
    li.innerText = start * step + i + 1;
    fragment.appendChild(li);
  }
  list.appendChild(fragment);
  start++;
}

function update() {
  render();
  let id = requestAnimationFrame(update);
  if (start >= inertCount) {
    cancelAnimationFrame(id);
  }
}
// 或者
async function update() {
  render();
  if (start < inertCount) {
    await scheduler.yield();
    update();
  }
}
update();

处理大量数据

  • 数据分页每次服务器端只返回一定数目的数据,浏览器每次只对一部分进行加载

  • 懒加载每次加载一部分数据,其余数据当需要使用时再去加载

  • 数组分块技术,为要处理的项目创建一个数组,然后设置定时器每过一段时间取出一部分数据进行处理,然后再使用定时器取出下一个要处理的项目进行处理

    function chunk(array, process, context) {
      setTimeout(function fn() {
        var item = array.shift();
        process.call(context,item);
        if (array.length > 0) {
          setTimeout(fn, 100);
        }
      }, 100);
    }
    var data = [12, 55, 336, 541, 2365, 22, 445, 226, 44];
    function printValue(item) {
      console.log(item);
    }
    chunk(data, printValue);
    

虚拟滚动

  • 渲染可视区域内的内容,非可见区域的那就完全不渲染
  • 用户在滚动的时候就实时去替换渲染的内容
  • 联系人列表,聊天列表

阻塞渲染

HTML 和 CSS 阻塞渲染

渲染的文件大小,并且扁平层级,优化选择器

  • JavaScript 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值
  • 如果浏览器尚未完成 CSSOM 的下载和构建,在此时运行脚本,那么浏览器将延迟 JavaScript 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建
  • CSS 会阻塞页面的渲染
    • 动态插入的外链 CSS 不会阻塞 DOM 的解析或渲染
    • 动态插入的内联 CSS 会阻塞 DOM 的解析或渲染
  • CSS 会阻塞 Javascript 执行

Script 暂停构建 DOM

当遇到 <script> 时,暂停 HTML 解析,加载解析执行 JS 代码。因为 JS 可能会改变 Html 的结构导致重新 reflow 和 repaint。

  • 同步的 JavaScript 都会阻塞 DOM 的解析或渲染
  • 异步的 JavaScript 不会阻塞 DOM 的解析或渲染
    • async 与 defer 的 JavaScript 脚本
    • 动态插入的外链 JavaScript 脚本

<script> 加 async 或 defer 属性,浏览器异步加载和运行 JS,不阻止解析。

  • async:指示浏览器尽可能异步加载脚本,默认同步加载脚本(async=false)

    • 没有任何依赖的 JS 文件,表示 JS 文件下载和解析不会阻塞渲染
    • Google Analytics
  • defer:指示脚本要在解析文档之后但在触发 DOMContentLoaded 之前执行。

    • 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,可以把 script 标签放在任意位置
    • 按照加载顺序执行脚本
    • 用 JS 模块化
    • 样式文件中加 rel = preload
    • 可设置资源加载优先级,优化加载渲染关键路径资源,优化性能

script 加载方式.png

预解析

  • 当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源(外部脚本、媒体文件、样式表、字体)
  • 并发请求资源数最多 6 个
  • 资源加载优先级
    • 资源的类型,如 CSS、script 这些阻塞渲染的优先级要高一些
    • 资源位于文档的位置有关:<head> 内的资源优先级要高一些

资源加载优先级.jpg

减少重绘和回流

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none
  • 不要把节点的属性值放在一个循环里当成循环里的变量
  • 不要使用 table 布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找避免节点层级过多
  • 频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点
    • will-change
    • video、iframe 标签
  • 不要一条一条地修改DOM的样式
  • 把DOM离线后修改 documentFragment
  • 动画的HTML元件使用fixed或absoult
  • 避免设置多层内联样式
  • 将动画效果应用到position属性为 absolute 或 fixed 的元素
  • 避免使用CSS表达式(例如:calc())
  • 先为元素设置display: none,操作结束后再把它显示出来
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来

渲染页面时常见哪些不良现象

FOUC

flash of unstyled content 页面闪烁

在 CSS 加载之前,先呈现了 HTML

  • css 加载时间过长
  • css 被放在了文档底部 可以隐藏body,当样式资源加载完成后再显示body
<!DOCTYPE html>
<html lang="en" class="no-js">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      /*modernizr会将html的no-js替换为js,并将modernizr代码在最后时加载,那么就能保证所有样式文件已经加载完成*/
      .no-js body {
        display: none !important;
      }
    </style>
  </head>
  <body>
    <h1>FOUC</h1>
    <script src="https://cdn.bootcdn.net/ajax/libs/modernizr/2.8.3/modernizr.min.js"></script>
  </body>
</html>

白屏

  • CSS 部分放在 HTML 尾部
  • 把 js 文件放在头部,脚本的加载会阻塞后面文档内容的解析
  • @import标签,它引用的文件则会等页面全部下载完毕再被加载

原因

  • 渲染的时候没有请求到或请求时间过长造成的
  • 浏览器对于JavaScript,在加载时会禁用并发,并且阻止其后的文件及组件的下载
  • 不同浏览器的处理CSS和HTML的方式是不同的
const timingInfo = window.performance.timing;
timingInfo.responseStart - timingInfo.fetchStart

关键渲染路径(CRP)

概念

Critical Rendering Path

关键渲染路径是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。

CRP 解析流程图.webp

评估

  • 关键资源: 可能阻止网页首次渲染的资源。
  • 关键路径长度: 获取所有关键资源所需的往返次数或总时间。
  • 关键字节: 实现网页首次渲染所需的总字节数 14kb

优化

  • CSS

    • 精简压缩 CSS
    • 将 CSS 位于 <head>
    • 内联阻塞渲染的关键 CSS
    • 使用 Media Query
    • 避免使用 CSS import 增加了关键路径中往往返次数
  • JavaScript

    • 优化精简 JavaScript、按需加载

    • JavaScript 脚本位于底部 图片等非关键资源的呈现时间却被延迟

    • async 与 defer

    • 字体文件使用 preload 提升优先级

      <link rel="preload" as="font" href="https://at.alicdn.com/t/font_zck90zmlh7hf47vi.woff">
      
  • 快使 DOMContentLoaded 事件产生

加载优先级

head img > head link css > body script > link css > body img > 内联 script > head link img

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="./img/1.png" />
    <img src="./img/2.png" />
    <link rel="stylesheet" href="./css/1.css" />
  </head>
  <body>
    <p>hello</p>
    <img src="./img/3.png" />
    <script src="./js/1.js"></script>
    <img src="./img/4.png" />
    <script>
      !(function () {
        let xhr = new XMLHttpRequest();
        xhr.open("GET", "https://baidu.com");
        xhr.send();
        document.write("hi");
      })();
    </script>
    <link rel="stylesheet" href="./css/9.css" />
  </body>
</html>

页面加载过程

页面加载过程.png

渲染指标.jpg

前端性能指标

FMP

First Meaningful Paint

页面主要内容出现在屏幕上的时间,用户最关注的的首屏内容显示

该指标的定义比较依赖于浏览器具体的实现细节不具有可参考的标准性

LT

长任务,当一个任务执行时间超过 50ms 时消耗到的任务,用户会感知到页面的卡顿

程序使用的体验(是否响应延迟,动画卡顿)

React Fiber

文档加载相关

TTFB

Time to First Byte

浏览器从请求页面开始到接收第一字节的时间,这个时间段内包括 DNS 查找、TCP 连接和 SSL 连接

timingInfo.responseStart - timingInfo.navigationStart;

DCL

DOMContentLoaded,当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发

timingInfo.domContentLoadedEventStart - timingInfo.navigationStart;

L

onLoaded,当依赖的资源,全部加载完毕之后才会触发

内容呈现相关

FP

First Paint(白屏)

网页的第一个像素渲染到屏幕上所用时间

const fistPaint = timingInfo.domComplete - timingInfo.fetchStart;

FCP

First Contentful Paint(首屏)

程序是否正确的开始渲染

  • 浏览器第一次向屏幕绘内容
  • 绘制文本、图片(包含背景图)、非白色的 canvas 或 SVG
  • 首次绘制来自 DOM 的内容
  • 字体加载

LCP

Largest Contentful Paint, 视窗最大可见图片或者文本块的渲染时间

LCP应该在2.5秒内

  • <img> 元素
  • <svg> 中的<imge>元素
  • <video> 元素(如果定义了封面图,会影响 LCP)
  • url() 背景图的元素
  • 块级元素有文本节点或者内联文本子元素
原则
  • 在 viewport 内可见元素的大小,如果是超出可视区域或者被裁减、遮挡等,都不算入该元素大小
  • 对于图片元素来说,大小是取图片实际大小和原始大小的较小值,即Min(实际大小,原始大小)
  • 对于文字元素,只取能够覆盖文字的最小矩形面积
  • 对所有元素,margin、padding、border 等都不算

交互响应性相关

TTI

Time To Interactive,网页第一次完全达到可交互状态的时间点

程序是否可用

最后一个长任务(Long Task)完成的时间, 并且在随后的 5 秒内网络和主线程是空闲的

TTI.png

tti-polyfill.js

FCI

First CPU Idle,页面第一次可以响应用户输入的时间

Lighthouse 不推荐

FID

First Input Delay

从用户第一次与页面交互(例如单击链接、点击按钮等)到浏览器实际能够响应该交互的时间

FCP和TTI之间

FID应该在100毫秒内

TBT

total blocking time,衡量从 FCP 到 TTI 之间主线程被阻塞时长的总和

CLS

Cumulative Layout Shift

  • 对整个视窗的多少造成了影响
  • 发生变化距离占整个视窗的比例
  • CLS 应该小于0.1

用户体验核心指标

  • 白屏时间
    • 页面开始有内容的时间,在没有内容之前是白屏
    • FP 或 FCP
  • 首屏时间
    • 可视区域内容已完全呈现的时间
    • FSP
  • 可交互时间
    • 用户第一次可以与页面交互的时间
    • FCI
  • 可流畅交互时间
    • 用户第一次可以持续与页面交互的时间
    • TTI

优化

优化 FP/FCP

  • <head> 移除影响 FP/FCP 的 css 和 js 代码
  • 将影响首屏渲染的关键 css 代码最小集合直接 inline 写在 <head>
  • 对 react 这种客户端渲染框架,做 ssr
  • 本地缓存

优化 FMP/TTI

  • 首先需要确定页面中的最关键元素,例如专题中的视频组件,然后需要保证关键组件相关的代码最先加载并且使得关键组件在第一时间被渲染且可交互
  • 图片懒加载,组件懒加载
  • 其他一些对渲染关键组件无用的代码可以延缓加载
  • 减少 html dom 个数和层数
  • 尽量缩减 FMP 和 TTI 的时间间隔,最好让用户知道当前页面并未完全可交互。如果用户想要交互但是页面没有响应,那么用户会感到不爽

防止 long tasks

  • 将代码分割,并对给不同代码分配不同的加载优先级。不仅能加快页面交互时间,而且可以减少 long tasks
  • 对于执行时间特别长的代码,可以尝试让他们分为几个异步执行的代码块

web-vitals

  • LCP
  • FID
  • CLS
var script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals/dist/web-vitals.iife.js';
script.onload = function() {
  // When loading `web-vitals` using a classic script, all the public
  // methods can be found on the `webVitals` global namespace.
  webVitals.getCLS(console.log);
  webVitals.getFID(console.log);
  webVitals.getLCP(console.log);
}
document.head.appendChild(script);
const po = new PerformanceObserver((entryList) => {
  let a = {};
  const entries = entryList.getEntries();
  entries.forEach((entry) => {
    if (entry.name === "first-paint") {
      a.FP = entry.startTime;
    } else if (entry.name === "first-contentful-paint") {
      a.FCP = entry.startTime;
    } else if (entry.entryType === "largest-contentful-paint") {
      a.LCP = entry.renderTime || entry.loadTime;
    }
  });
  console.log(a);
});
po.observe({
  entryTypes: ["largest-contentful-paint", "paint", "longtask"],
});

用户体验

  • 0到16ms

    用户希望看的动画能够流畅,,浏览器上每秒钟渲染60帧动画就能够保持流畅,这大约就是16ms渲染一帧,这16ms包括了浏览器要渲染新的元素到页面上需要的时间,也就是说程序有大约10ms的时间可以进行操作。

    动画:生产每一帧动画的时间在10ms左右

  • 0到100ms

    在这个时间内响应用户的交互,用户会觉得响应是非常及时的

    响应时间:处理响应用户的操作在50ms以内

  • 100到300ms

    用户会感觉到有一些延迟

  • 300到1000ms

    当执行一些页面加载或者页面跳转的时候,在这个时间内是一个正常的加载跳转时间

  • 1000ms或以上

    超过1000ms(1秒),用户会对之前的操作渐渐失去耐心和注意力

  • 10000ms或以上

    当你的响应超过10秒,用户会感到烦躁,然后终止之前操作

    页面加载时长不超过 5 秒

  • 主线程闲置时间越多越好

  • 主要内容渲染完成且程序可交互时间在5秒之内

性能是如何影响商业的

有更快可交互时间的用户是否会买更多商品

在付款时如果有更多的 Long Tasks,用户是否有更高的概率放弃

场景观察顺序

标准流程

标准流程.jpg

  • FC: 橘色DIV渲染出来了
  • FCP: 有内容的渲染开始是 Img
  • FMP: 相差并不大
  • LCP: Largest Contentful Paint 即图片完全都加载出来了
  • DCL: DOM Ready DOM解析彻底完成, 即完成 Render Tree 的绘制

CSR

Client Side Rendering

CSR.jpg

  • 使用loading作为FP, FCP 首屏有内容的输出
  • css, js 以及到最后 DOM 全部解析完成
  • DCL: 可以执行JS, 并挂载到 id="app" 这里
  • FMP: 导航栏的输出,被标记为有内容的输出
  • L: 所有资源均加载完成,图片显示在屏幕上
  • LCP: 占地儿最大,最有意义的输出。

异步导入 js

DCL提前

CSR异步加载.jpg

服务端渲染

FCP / FMP 首屏有效内容提前输出

服务端渲染.jpg

RAIL 模型

  • Response

    100ms 内响应用户输入

  • Animation

    动画或者滚动需在 10ms 内产生下一帧

  • Idle

    最大化空闲时间

  • Load

    页面加载时长不超过 5 秒

指标

  • 页面初始访问速度 + 交互响应速度:白屏、首屏时间、可交互时间
    • FP/FCP
    • FMP
    • TTI
    • LT
  • 页面稳定性:页面出错情况
    • 资源加载错误
    • JS 执行报错
  • 外部服务调用,网络请求访问速度
    • CGI 耗时
    • CGI 成功率
    • CDN 资源耗时

懒加载

概念

  • 延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式
  • 用户滚动到它们之前,可视区域外的图像不会加载
  • 适用图片很多,页面很长的电商网站场景

意义

  • 能提升用户的体验
  • 减少无效资源的加载 减轻服务器和浏览器压力
  • 防止阻塞 js 的加载

原理

  • 将页面上的图片的 src 属性设为空字符串
  • 真实路径设置在data-original属性中
  • 页面滚动的时候需要去监听scroll事件
  • scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域
  • 图片在可视区内将图片的 src 属性设置为data-original 的值
<img
  src=""
  class="image-item"
  lazyload="true"
  data-original="https://img0.baidu.com/it/u=3437217665,1564280326&fm=26&fmt=auto"
/>
.image-item {
  display: block;
  margin-bottom: 50px;
  /* 一定记得设置图片高度 */
  height: 200px;
}
var viewHeight = document.body.clientHeight; //获取可视区高度
function lazyload() {
  var eles = document.querySelectorAll("img[data-original][lazyload]");
  console.log(eles);
  Array.prototype.forEach.call(eles, function (item, index) {
    var rect;
    if (item.dataset.original === "") return;
    rect = item.getBoundingClientRect(); // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
    if (rect.bottom >= 0 && rect.top < viewHeight) {
      (function () {
        var img = new Image();
        img.src = item.dataset.original;
        img.onload = function () {
          item.src = img.src;
        };
        item.removeAttribute("data-original"); //移除属性,下次不再遍历
        item.removeAttribute("lazyload");
      })();
    }
  });
}
lazyload(); //刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll", lazyload);

预加载

概念

所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源

意义

在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验

原理

  • 使用HTML标签

    display:none

    <img
      src="https://img0.baidu.com/it/u=3437217665,1564280326&fm=26&fmt=auto"
      style="display: none"
    />
    
  • 使用Image对象

    var image = new Image();
    image.src = "https://img0.baidu.com/it/u=3437217665,1564280326&fm=26&fmt=auto";
    
  • 使用XMLHttpRequest对象

    • 存在跨域问题
    • 精细控制预加载过程
    • 进度和完成事件的更好支持
    //   使用XMLHttpRequest对象,虽然存在跨域问题,但会精细控制预加载过程
    var xmlhttprequest = new XMLHttpRequest();
    xmlhttprequest.onreadystatechange = callback;
    xmlhttprequest.onprogress = progressCallback;
    xmlhttprequest.open(
      "GET",
      "https://img0.baidu.com/it/u=3437217665,1564280326&fm=26&fmt=auto",
      true
    );
    xmlhttprequest.send();
    function callback() {
      if (xmlhttprequest.readyState == 4 && xmlhttprequest.status == 200) {
        var responseText = xmlhttprequest.responseText;
      } else {
        console.log("Request was unsuccessful:" + xmlhttprequest.status);
      }
    }
    function progressCallback(e) {
      e = e || event;
      if (e.lengthComputable) {
        console.log("Received " + e.loaded + " of " + e.total + " bytes");
      }
    }
    
  • 使用 PreloadJS

减小内容数量和大小

  • 通过文件合并、css 雪碧图、使用 base64 等方式来减少 HTTP 请求数,避免过多的请求造成等待的情况

    低于256色以适用PNG8格式

  • 通过设置缓存策略,对常用不变的资源进行缓存

  • 通过对 JavaScript 和 CSS 的文件进行压缩,来减小文件的体积

  • 减少 DOM 元素数量

    能通过伪元素实现的功能,就没必要添加额外元素

  • 使用高效的事件处理

    • 减少绑定事件监听的节点,如通过事件委托
    • 尽早处理事件,在DOMContentLoaded即可进行,不用等到load以后
  • 不要使用 <img> 的 width、height 缩放图片

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

    • 存在(避免 404)
    • 尽量小,最好小于 1K
    • 设置较长的过期时间
  • 移动端

    • 保证所有组件都小于 25K

      iPhone 不能缓存大于 25K 的组件

    • 打包内容为分段(multipart)文档

减少请求数量和大小

  • 使用 CDN 服务,来提高用户对于资源请求时的响应速度
  • 服务器端启用 Gzip(GNUzip)、Deflate 等方式对于传输的资源进行压缩,减小文件的体积
  • 尽可能减小 cookie 的大小,并且通过将静态资源分配到其他域名下,来避免对静态资源请求时携带不必要的 cookie
    • 减少 Cookie 大小
    • 静态资源使用无Cookie域名
  • 通过 DNS 缓存等机制来减少 DNS 的查询次数
    • 使用 Keep-Alive
    • 使用较少域名
  • 避免重定向根据响应头中Location的地址再次发送请求
    • URL 末尾应该添加/但未添加
    • 3xx HTTP 状态码,主要是为了让返回按钮能正常使用
  • 缓存判断顺序
  • 避免404错误
  • Ajax 尽量请求使用GET方法
  • 避免使用空的链接,js 阻止默认行为

提前渲染页面

  • 把样式表放在页面的 head 标签中,减少页面的首次渲染的时间
  • 避免使用 @import 标签
  • 尽量把 js 脚本放在页面底部或者使用 defer 或 async 属性,避免脚本的加载和执行阻塞页面的渲染
  • 使用延迟加载的方式,来减少页面首屏加载时需要请求的资源延迟加载的资源当用户需要访问时,再去请求加载
  • 通过用户行为,对某些资源使用预加载的方式,来提高用户需要访问资源时的响应速度

setInterval

setInterval(fn(), N);

推入任务队列后的时间不准确

fn() 将会在 N 秒之后被推入任务队列

缺点

  • 使用 setInterval 时,某些间隔会被跳过
  • 可能多个定时器会连续执行

js Timeline.png

setInterval 模拟 setTimeout

function mySetTimeout(fn, wait) {
  let timer = setInterval(() => {
    fn();
    clearInterval(timer);
  }, wait);
  return timer
}
let timerId = mySetTimeout(() => {
  console.log(1);
}, 6000);
// clearTimeout(timerId)

setTimeout

// 执行完同步任务以后才会执行setTimeout里的任务
let startTime = new Date().getTime();
setTimeout(() => {
  let endTime = new Date().getTime();
  console.log(endTime - startTime);
}, 50);
for (let i = 0; i < 10000; i++) {
  console.log(i);
}

setTimeout模拟setInterval

function mySetInterval(fn, wait) {
  let timerId = null;
  function interval() {
    clearTimeout(timerId);
    fn();
    timerId = setTimeout(interval, wait);
  }
  timerId = setTimeout(interval, wait);
  return {
    clear: () => {
      clearTimeout(timerId);
    },
  };
}
function myClearInterval(timer) {
  timer.clear();
}
const timer = mySetInterval(() => {
  console.log("1");
}, 1000);
// myClearInterval(timer);

requestAnimationFrame

概念

  • 回调函数传入
  • 在下次重绘之前调用指定的回调函数更新动画
  • 回调函数会在浏览器下一次重绘之前执行
    • 回调函数执行次数通常与浏览器屏幕刷新次数相匹配

      刷新率和 requestAnimationFrame 存在不同步问题

      • Linux下的Nvdia驱动进行60FPS 硬编码
      • 多显示器
    • 后台标签页或者隐藏的 <iframe> 暂停执行

  • 下次重绘之前继续更新下一帧动画回调函数自身必须再次调用
  • 浏览器的显示频率是 16.7ms = 1000 / 60
  • 多个回调会统一时间执行

使用场景

  • 提升性能,防止掉帧

    • 回调函数在屏幕每一次刷新间隔中只被执行一次
    • 倒计时替换 setInterval 和 setTimeout
  • 节约资源,节省电源

    当页面处于未激活的状态下任务会被挂起

  • 函数进行节流

API

  • callback(DOMHighResTimeStamp)

    • DOMHighResTimeStamp 指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间 1000 / 60 = 16.7
    • 最小精度为1ms
  • 返回值

    • 一个 long 整数 id
    • window.cancelAnimationFrame()

life of a frame.png

requestAnimationFrame 模拟 setTimeout 和 setInterval

const RAF = {
  intervalTimer: null,
  timeoutTimer: null,
  setTimeout(fn, wait) {
    // 实现setTimeout功能
    let now = Date.now;
    let stime = now();
    let etime = stime;
    let loop = () => {
      this.timeoutTimer = requestAnimationFrame(loop);
      etime = now();
      if (etime - stime >= wait) {
        fn();
        cancelAnimationFrame(this.timeoutTimer);
      }
    };
    this.timeoutTimer = requestAnimationFrame(loop);
    return this.timeoutTimer;
  },
  clearTimeout() {
    cancelAnimationFrame(this.timeoutTimer);
  },
  setInterval(fn, wait) {
    // 实现setInterval功能
    let now = Date.now;
    let stime = now();
    let etime = stime;
    let loop = () => {
      this.intervalTimer = requestAnimationFrame(loop);
      etime = now();
      if (etime - stime >= wait) {
        stime = now();
        etime = stime;
        fn();
      }
    };
    this.intervalTimer = requestAnimationFrame(loop);
    return this.intervalTimer;
  },
  clearInterval() {
    cancelAnimationFrame(this.intervalTimer);
  },
};

function a() {
  console.log(1);
}
RAF.setTimeout(a, 1000);

setTimeout 模拟 requestAnimationFrame

(function () {
  var lastTime = 0;
  window.requestAnimationFrame = function (callback, element) {
    var currTime = new Date().getTime();
    //   帧率
    var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
    console.log(16.7 - (currTime - lastTime));
    var id = window.setTimeout(function () {
      // 调用时候的时间戳
      callback(currTime + timeToCall);
    }, timeToCall);
    lastTime = currTime + timeToCall;
    return id;
  };
  window.cancelAnimationFrame = function (id) {
    clearTimeout(id);
  };
})();