如何查找和解决前端内存泄漏问题? - 排查和分析技巧详解

4,066 阅读14分钟

我正在参加「掘金·启航计划」

前言

写这篇文章的起因是:老早之前在公司接手了一款同事开发的 Electron 应用程序,基于 Vue 的。客户反馈该应用使用一段时间就会白屏失去响应,必须强制关闭重启应用才能继续使用。因为刚接手且之前没碰到过这种问题,所以开始也是一头雾水,从一开始以为是操作系统问题(客户使用的是 win7 32位操作系统,开发时用的 win10 64位操作系统),到后来以为是 Electron 版本问题,最终才定位到是内存泄漏导致的。在此做一总结,因本人能力水平有限,如有错误和建议,欢迎在评论区指出。若本篇文章有帮助到了您,不要吝啬您的小手还请点个赞再走哦!

1 什么是前端内存泄漏

前端内存泄漏是指在网页或应用程序中,由于代码错误或者其他原因,导致分配给页面或应用程序的内存无法被垃圾回收器回收。这会导致内存使用量不断增加,最后可能导致应用程序崩溃或者变得超级缓慢。内存泄漏问题一旦发生,不仅会对网页或应用程序的性能造成负面影响,还会影响用户的体验。

2 引起内存泄漏的常见原因

2.1 意外的全局变量

由于 js 对未声明变量的处理方式是在全局对象上创建该变量的引用。如果在浏览器中,全局对象就是 window 对象。变量在窗口关闭或重新刷新页面之前都不会被释放,如果未声明的变量缓存大量的数据,就会导致内存泄露。

示例1:变量未声明

function fn() {
  a = new Array(1000).fill('666')
}
fn()

示例2:使用 this 创建的变量(全局作用域下的 this 的指向 window)。

function fn() {
  this.a = new Array(1000).fill('888')
}
fn()

解决方法:

  • 尽可能避免使用全局变量,在开发中我们可以使用严格模式或者通过 lint 检查来避免这些情况的发生,从而降低内存成本。
  • 对于必须要使用的全局变量,在使用完将其置为 null ,从而触发 GC 垃圾回收。特别是在使用全局变量做持续存储大量数据的缓存时,我们一定要记得设置存储上限并及时清理,不然的话数据量越来越大,内存压力也会随之增高。

2.2 未正确使用闭包

JavaScript中 的闭包指的是一个函数能够访问并使用其定义时所在的词法作用域(即函数定义时的作用域),即使这个函数在定义时所在的作用域已经被销毁了,这个函数仍然可以访问到它定义时所访问的变量、函数和参数。

举个例子来说,假设有一个函数generateClosure,它返回了一个内部函数innerFn,并且innerFn使用了外部函数的变量x:

function generateClosure() {
  let x = new Array(1000).fill('888');
  function innerFn() {
    console.log(x);
  }
  return innerFn;
}
​
var closure = generateClosure();
closure(); 

在上面的例子中,在执行closure函数时,闭包中存储了generateClosure的词法作用域,因此innerFn能够访问和使用generateClosure的变量x。注意,当generateClosure函数完成执行并返回innerFn时,generateClosure的词法作用域并没有被销毁,因为innerFn持有了对它的引用。

2.3 游离的 DOM引用

DOM对象是占用内存最高的一类对象之一,因此如果在应用程序中频繁地创建和销毁DOM对象,就容易导致内存泄漏。游离的DOM引用指的是已经不在文档中的DOM节点的引用,但是这些引用仍然被保存在JavaScript的变量、数组和对象中,因此这些DOM节点无法被垃圾回收器回收,从而导致内存泄漏。

以下是一个例子说明游离的DOM引用如何造成内存泄漏:

function test() {
  let el = document.createElement("div");
  document.body.appendChild(el);
  let child = document.createElement("div");
  el.appendChild(child);
  
  document.body.removeChild(el) // 由于 el 变量存在,el及其子元素都不能被GC
  el = null;   // 虽置空了 el 变量,但由于 child 变量引用 el 的子节点,所以 el 元素依然不能被GC
  child = null; // 已无变量引用,此时el可以GC
  
}
​
test();

如上所示,当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。

2.4 事件监听器未移除

当事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。

这里以Vue为例:

<template>
  <div></div>
</template><script>
export default {
  created() {
    window.addEventListener("mouseup", this.doSomething)
  },
  beforeDestroy(){
    window.removeEventListener("mouseup", this.doSomething)
  },
  methods: {
    doSomething() {
      // do something
    }
  }
}
</script>

2.5 循环引用

  1. 如果两个对象相互引用,且不存在其他对象对它们的引用,就会导致这两个对象无法被正常释放,从而导致内存泄漏。
function test() {
  var obj1 = {};
  var obj2 = {};
  obj1.prop = obj2;
  obj2.prop = obj1;
}
test()

2.6 定时器未清理

前端中定时器使用不当很容易造成内存泄漏。以下是一个例子,说明定时器使用不当如何造成内存泄漏:

function test() {
  var count = 0;
  var timer = setInterval(function() {
    console.log(++count);
  }, 1000);   // 每1秒输出一个数字
}
​
test();

在上述代码中,程序使用 setInterval 方法创建定时器 timer ,每1秒输出一个数字到控制台。如果不将 timer 清除,那么程序将会继续在后台运行,无法被垃圾回收器回收,从而导致内存泄漏。

要避免通过定时器造成的内存泄漏,需要正确地清除定时器,通常可以使用 clearInterval 方法来清除定时器:

function test() {
  var count = 0;
  var timer = setInterval(function() {
    console.log(++count);
    if (count === 10) {
      clearInterval(timer);
    }
  }, 1000);   // 每1秒输出一个数字,共输出10次
}
​
test();

在上述代码中,程序清除了定时器 timer ,当输出的数字达到10的时候就清除定时器,避免了内存泄漏的问题。

2.7 eventBus 未清理

在 Vue 应用中,eventBus 是常用的组件通信方式之一,但如果使用不当,也会导致内存泄漏。以下是一个例子,说明 eventBus 使用不当如何造成内存泄漏:

eventBus.js

import Vue from 'vue';
export const eventBus = new Vue();

component1.vue

import { eventBus } from './eventBus';
export default {
  data() {
    return {
      message: '',
    };
  },
  created() {
    eventBus.$on('updateMessage', this.updateMessage);
  },
  destroyed() {
    eventBus.$off('updateMessage', this.updateMessage);
  },
  methods: {
    updateMessage(newMessage) {
      this.message = newMessage;
    }
  }
}

component2.vue

import { eventBus } from './eventBus';
export default {
  data() {
    return {
      message: 'Hello',
    };
  },
  mounted() {
      eventBus.$emit('updateMessage', this.message);
  }
}

如上,我们只需在 component1.vue 中的 beforeDestroy 组件销毁生命周期里将其清除即可。

2.8 使用一些插件时,未调用销毁函数

如 sortablejs

const sortableInstance = Sortable.create(……)
sortableInstance.destroy()

调用 destroy() 以移除插件中创建的事件及对象,否则可能造成内存泄漏。

2.9 未清理的Console输出

当开发人员使用 Console 输出大量数据时,这些数据可能会残留在浏览器的内存中,并在长时间的使用后导致浏览器变慢,并占用大量的内存。虽然这些数据不会直接导致内存泄漏,但是会降低应用程序的响应速度和性能,影响用户体验。

另外,如果 Console 输出包含敏感信息,如用户名、密码等,那么这些信息就有可能被他人获取,从而产生安全问题。

为了避免这些问题,开发人员应该在 Console 输出大量数据时,注意将这些数据清除,以释放浏览器的内存资源和保护用户的隐私。可以使用 console.clear() 方法清空 Console 记录,并避免在 Console 输出包含敏感信息的数据。

3 如何排查内存泄漏

看到这里,貌似我们已经知道了造成内存泄漏的大部分原因。那么到底我们该如何排查呢?难道只能通过一行行代码去排查吗,当然不是,谷歌的开发者工具也就是我们所说的浏览器控制台(Chrome Devtool)功能其实十分强大,通过它可以帮助我们分析程序中像性能、安全、网络等各种东西,也可以让我们快速定位到问题源,只是很多人并不熟悉其使用而已。

如果你是网页版,首先应该开启浏览器的无痕模式,无痕模式默认不会加载插件,这样可以排除浏览器插件对性能的影响。我先前是直接在Electron中调试的,原理都是一致的。

image-20230509182215377.png

注:下文中调试都是基于 Chrome 89 内核版本的,如果你使用的是其他版本的Chrome,Chrome Devtool 可能会有些许差异。

3.1 页面概况分析:Performance 面板

一般可以先在 Performance 面板 中获取到页面堆内存随着时间流逝的图像,据此分析页面操作和内存泄漏的总体情况,然后再通过 Memory 面板进行内存泄漏的详细分析。

使用方法:

  1. 首先打开Chrome Devtool 开发者工具,点击进入到 Performance 面板,勾选上 ScreenshotsMemory 选项,点击箭头所指的 record 按钮开始记录页面参数信息,在此过程中可以进行一些内存泄漏相关的可疑操作,方便后续的分析。

image-20230509183849124.png

  1. 点击 stop 按钮结束记录,接着会生成涵盖此过程中页面各种参数的图表。

image-20230509183943413.png

注意在记录之前及记录结束时都要按一下上方的垃圾桶图标进行手动垃圾回收,如果结束时js heap图像没有下降,一直是阶梯式增长,那么很有可能页面存在内存泄漏。

生成的图表示例:

Snipaste_2023-05-09_18-46-09.png 图表中的关键信息:

  • FPS:(最上方第一排)最上方为页面运行时的帧数记录,绿色时表示帧数较高,动画较流畅
  • CPU:(最上方第二排)CPU 的占用情况记录
  • NET:(最上方第三排)网络请求情况
  • 操作时的动画帧:(最上方第四排)鼠标悬停此处可以查看当前帧图像,及对应的FTP、CPU、HEAP等情况
  • HEAP:(最上方第五排)堆内存的占用情况

从上方红框内的折线图可知:

因为js heap, documents, nodes都在呈阶梯性上升。通过对比该部分图像及上方操作时的动画帧可以粗略分析出内存泄漏的位置。

3.2 更加精准的定位:Memory 面板

Chrome DevTools Memory 通过页面的 JavaScript 对象和相关的 DOM 节点显示内存分布。使用它来获取JS堆快照,分析内存图,比较快照,并查找内存泄漏。

生成堆快照(Heap snapshot)

记录之前点击一点垃圾桶图标,强制触发一次垃圾回收,这样可以更加准确的获取到结果。根据下图中序号的次序开始记录:

箭头所指含义:

  1. 先点一次它触发垃圾回收。
  2. 选择此类型来记录堆快照。
  3. 点击记录的按钮生成堆快照文件,后续可以进行分析和对比。

image-20230510171847118.png

当快照加载到DevTools并被解析后,在快照标题下面的数字会出现,并显示可访问JavaScript对象的总大小。快照中只包含可访问对象。此外,获取快照总是从垃圾回收开始,如下图:

image-20230510172539373.png

顶部下拉选项含义:

image-20230510172849874.png

  • Summary

    显示按构造函数名称分组的对象。使用它根据构造函数类型查找对象(以及它们的内存使用)。它对于跟踪 DOM 内存泄漏特别有用。

  • Comparison

    显示两个快照之间的差异。使用它来比较一个操作前后的两个内存快照(在右侧下拉选项中选择要和当前对比的内存快照)。通过检查已释放内存中的增量和引用计数,可以确认内存泄漏的存在及其原因。

  • Containment

    此选项提供了一个更好的对象结构视图,帮助分析全局命名空间(window)中引用的对象,以找出是什么使它们保持在内存中。使用它来分析闭包,并从较低的层次深入到对象中。

  • Statistics

    统计各个类型的大小。如下图所示:

image-20230510173148229.png

表头含义:

  • Constructor 表示使用此构造函数创建的所有对象。Constructor 列详细解释如下:

    • 构造函数行 ×符号 后的数字 实例对象的个数
    • 构造函数行展开后的 @符号 后的数字 对象的唯一ID,允许您在每个对象的基础上比较堆快照
    • global 在全局对象(如'window')和被它引用的对象之间的中间对象。如果一个对象是使用构造函数 Person 创建的,并且被全局对象持有,则保留的路径类似于[global] >(全局属性)> Person。
    • closure 通过函数闭包对一组对象的引用计数
    • array, string, number, regexp 具有引用Array、String、Number或正则表达式的属性的对象类型列表
    • compiled code 简单地说,就是所有与编译代码相关的内容。
    • HTMLDivElement, HTMLAnchorElement, DocumentFragment 等 对代码引用的特定类型的元素或文档对象的引用。
    • system 指定为 system 的对象没有相应的 JavaScript 类型。它们是JavaScript VM对象系统实现的一部分。不能从JavaScript代码访问。
  • Shallow size 展示某个构造函数创建的所有对象的shallow size 之和。shallow size 是指对象本身所持有的内存大小(通常,数组和字符串具有更大的浅大小)。

  • Retained size 一旦一个对象被删除(并且它的依赖项不再可访问),可以释放的内存大小被称为 retained size。

  • Distance 节点的最短简单路径到根节点的距离。

堆快照对比分析示例:

进行一些内存泄漏的可疑操作后继续记录堆快照,我这里总共记录了 4 个堆快照,后续我会选中第四个堆快照。如图所示:

image-20230510175429009.png

上图在选中 第 4 个堆快照(snapshot 6) 的基础上,选择如图所示的 comparision 和 snapshot1,然后分析 snapshot 6 和 snapshot 1 两个快照之间的堆内存差异。

image-20230510175512589.png

上图在选中 第 4 个堆快照(snapshot 6) 的基础上和 第2个堆快照(snapshot 4)对比。

image-20230510175621833.png

上图在选中 第 4 个堆快照(snapshot 6) 的基础上和 第3个堆快照(snapshot 5)对比。

可以看出 closure 闭包 和 array 数组是内存增长的两个大头,点击垃圾回收图标后再次记录一下堆快照:

image-20230510180248560.png

和 snapshot 6 对比发现释放的内存非常少,所以我们可以从closure 和 array 展开后的对象开始查看分析一下。

Allocation instrumentation on timeline 工具

接着上面的堆快照来,我们现在使用一下 Allocation instrumentation on timeline 工具来查找没有被正确地垃圾回收的对象。首先清空堆快照:

image-20230510180701252.png 选中 Allocation instrumentation on timeline ,然后点击左上角按钮开始记录,并在此期间进行可能造成内存泄漏的操作。在操作的过程中,注意蓝色柱条的生成时机,蓝色柱条生成很有可能对应着造成内存泄漏的操作。

image-20230510180813059.png

分析最后生成的统计图像:

image-20230510180842359.png 上图中每个柱条的高度对应最近分配的对象的大小,柱条的颜色表示这些对象是否仍然在最后的堆快照中。蓝色条表示在时间轴结束时仍然存活的对象,灰色条表示在时间轴期间分配但已经被垃圾回收的对象。

image-20230510180953290.png

点击图中的蓝色柱条可以在下方查看对应选择范围的构造对象详情。鼠标滚轮可以缩放选择范围的大小。

重复上面的步骤,记录多个时间轴统计图作对比分析:

image-20230510181237493.png

image-20230510181245565.png

snapshot1 和 snapshot2 对比,发现图中标注的 Object、VueComponent、Closure等都有比较明显的增加。

image-20230510181312743.png

可以选中 snapshot2 中的蓝色柱条对此范围内的对象进行详细查看。

4 Electron 应用中的一些排查方法示例 (4.3、4.4 节 Web 端通用)

本节介绍一些我自己在 Electron 程序中使用过的排查、调试方法。供读者参考。

4.1 监听渲染进程意外消失的方法:

当内存泄漏造成渲染进程崩溃时,会触发程序的 render-process-gone 的事件,这时候可以相应的做一些处理。

    // 渲染器进程意外消失时触发。 这种情况通常因为进程崩溃或被杀死。
    win.webContents.on('render-process-gone', (event, details) => {
        if (typeof details === 'object') {
            details = JSON.stringify(details)
        }
​
        saveLog(
            `接收时间: ${new Date().toLocaleString()}` +
                '\n' +
                'render-process-gone 触发--> details:' +
                details +
                '\n\n'
        )
    })

因我公司开发的是医疗软件,生产环境下是不连外网的,所以日志都是记录在本地的。如果要发送崩溃日志,自己写一下对应逻辑即可。

之前还写过一个监听到渲染器进程意外消失时,自动重启应用或重新加载页面的方法,可以参考一下:

   // 渲染器进程意外消失时触发。 这种情况通常因为进程崩溃或被杀死。
    win.webContents.on('render-process-gone', (event, details) => {
        if (typeof details === 'object') {
            details = JSON.stringify(details)
        }
​
        saveLog(
            `接收时间: ${new Date().toLocaleString()}` +
                '\n' +
                'render-process-gone 触发--> details:' +
                details +
                '\n\n'
        )
        
        // 用于处理 Electron 应用程序窗口重启或重新加载的逻辑,并保证只有当前活跃窗口被重启或重新加载。
        // 首先,判断当前窗口是否已经被销毁,如果窗口已经被销毁,则调用 app.relaunch() 方法进行应用程序重启,并通过 app.exit(0) 方法退出当前进程。
        if (win.isDestroyed()) {
            app.relaunch()
            app.exit(0)
        } else {
            // 如果当前窗口未被销毁,遍历所有窗口,
            BrowserWindow.getAllWindows().forEach((w) => {
                // 判断是否为当前窗口,如果不是当前窗口,就通过调用 w.destroy() 方法销毁其他窗口。
                if (w.id !== win.id) w.destroy()
            })
            // 调用当前窗口的 win.webContents.reload() 方法来重新加载当前窗口所绑定的网页。
            win.webContents.reload()
        }
    })

4.2 主进程 crashReporter 模块

该模块能生成 Dump 文件,可自行配置上传到服务器,用以分析程序崩溃原因,详情请查看文档:

文档地址:www.electronjs.org/docs/latest…

4.3 performance monitor 工具

如图:在Electron 程序的开发阶段,可以打开 performance monitor 工具,打开后会在下方显示一个CPU 使用率、JS 堆内存、DOM 节点数等信息的实时面板,然后可以自己手动进行一些可疑操作,观察 JS 堆内存、DOM节点数的实时变化,如果在某些操作下,JS 堆内存持续增长,没有被自动垃圾回收,那么就很有可能这些操作造成了内存泄漏。

image-20230512101845277.png

当然,总是自己手动去点,也太麻烦了,毕竟重复性的工作最好能交给计算机去做。我当时是借助一款录屏脚本软件,可以记录鼠标及键盘的操作,来进行自动化测试。软件名称叫:MacroRecorder。比如说我当时测试了一百次可疑操作后,JS 堆内存涨到接近 1000MB了,DOM 节点数也一直在上涨,这肯定是不正常的。如图:

image-20220304171018174.png

可通过测试结果结合操作再做进一步的分析。

4.4 Web API:window.performance

window.performance 是 Web API 中的一个重要接口,用于提供网页性能检测数据。它可以让开发者在浏览器中准确地测量网页性能、分析网页性能瓶颈并进行优化。该接口提供了一组方法和属性,可以用于测量页面加载时间、响应时间、资源性能等指标。

window.performance 主要属性和方法介绍:

Performance.timing:是一个只读属性,用于提供与文档有关的时间信息。 包括以下属性:

  • NavigationStart: 记录浏览器开始加载文档的时间;
  • Redirect: 重定向的时间;
  • AppCache: 在检查应用程序缓存之前花费的时间;
  • DNS: 域名解析的时间;
  • TCP: TCP 连接的时间;
  • Request: 发送 HTTP 请求的时间;
  • Response: 接收 HTTP 响应数据的时间;
  • DomLoading: 开始解析 HTML 数据的时间;
  • DomInteractive:完成解析 HTML 数据但是 Document 对象没有完全建立的时间;
  • DomContentLoaded: DOMContentLoaded 的事件被触发的时间;
  • LoadEvent: load 事件完成的时间;

Performance.now():Performance.now() 方法返回当前时间和 Performance.timing.navigationStart 的差值,但是精度是微秒级别的。在同一次页面运行过程中不会发生重置。

Performance.memory:Performance.memory 是一个只读属性,返回内存使用情况的统计信息,包括:

  • jsHeapSizeLimit:浏览器为 JavaScript 堆设置的最大堆内存限制;
  • totalJSHeapSize:当前 JavaScript 堆的总大小;
  • usedJSHeapSize:当前 JavaScript 堆中使用的内存大小。

关于 window.performance 的使用,我自己当时主要是记录了:一些特定操作下的 window.Performance.memory 中的堆内存情况。window.performance 的功能很强大,各位看官如有需要,可以查阅一下相关文章,再做进一步的应用。

4.5 我是怎么解决的

在这里我把自己当时做的一些排查方案按顺序记录一下,供大家参考,可能不是很完善,毕竟时间已经过去了很久,而我也是个小菜鸟。

  1. 根据客户的操作系统类型、electron版本等,用搜索引擎、在官网、github issues 查找有无相关案例。

    结果:此方法没有什么实质性发现。

  2. 根据第 2 节内容在项目中全局搜索,对可疑操作部分代码进行局部的详细分析。

    结果:确实发现代码中有事件监听器未移除,eventBus 未清理。但是事与愿违,处理后仍然有内存泄漏现象。

  3. 根据第 4 节内容在程序中添加监听渲染进程消失的方法,添加 crashReporter 模块。

    结果:渲染进程崩溃时捕获到日志及dump文件,发现崩溃原因为 OOM(out of memory),即内存溢出。

  4. 根据第 3 节借助 Chrome Devtool 进行详细的分析和定位。

    结果:可能是因为第 3 方库的影响,要定位到项目代码部分依然困难重重。

  5. 根据第 4.3、4.4 节方法,再做进一步测试。

    结果:基本定位到了是什么模块、什么操作引起了内存泄漏。对代码进行了进一步优化。

  6. 很是崩溃,走到这里依然没有完全解决问题,还是会出现内存泄漏导致白屏的情况,只是频率降低了。之前已经分析出了,主要是列表和详情界面频繁切换导致内存不断上涨,详情界面有一个比较复杂的可视化 svg 组件,可能是频繁的创建和销毁这个组件导致了内存泄漏,但是组件中并未发现任何异常的代码。没辙了吗?

  7. 最终解决方案:详情界面采用 keep-alive 组件,避免频繁的创建和销毁复杂组件,在详情界面跳转到列表界面时再将 详情界面的所有 data 数据初始化,避免变量污染,最终内存溢出导致的白屏问题彻底解决了。(我也想不到,最终会以这种结局收场,不过好在过程中学到了很多东西,还是有很多收获的!)

5 内存问题的类型及性能模型

最后再介绍一下关于内存 和 性能的两个概念吧。

5.1 内存问题的类型

内存问题不光只有内存泄漏,主要可以分为三类,内存问题很重要,因为它们通常是用户可以感知的。用户可以通过以下方式感知内存问题:

  • 随着时间的推移,页面的性能会越来越差 这可能是内存泄漏的症状。内存泄漏是指页面中的bug导致页面随着时间的推移逐渐使用越来越多的内存。
  • 页面的性能总是很差 这可能是内存膨胀的症状。内存膨胀是指页面使用的内存超过最佳页面速度所需的内存。
  • 页面的性能被延迟或似乎经常暂停。 这可能是频繁的垃圾回收的症状。垃圾回收是指浏览器回收内存。浏览器决定何时发生这种情况。在收集期间,所有脚本执行都将暂停。因此,如果浏览器经常进行垃圾收集,脚本执行就会经常暂停。

5.2 RAIL 性能模型

RAIL 性能模型是一个用于衡量 Web 应用程序性能的框架,该模型由 Google 提出。RAIL 的全称是 Response, Animation, Idle, Load,也就是响应、动画、空闲和加载。

RAIL 将 Web 应用程序的性能分为四个方面:

  1. 响应(Response):用户交互的时间需要在 100 毫秒以内响应完成,否则会引起用户操作等待,这会降低用户的体验和满意度。
  2. 动画(Animation):高端设备大约需要 16 毫秒一帧来进行动画显示,而更普遍的设备需要 24 毫秒以上来进行同样的操作。超过这个时间就可能会引起视觉上的不连续,影响用户的观感。
  3. 空闲(Idle):充分利用空闲时间做一些更为复杂和耗时的事情,例如预先解压资源、执行资源懒加载等。
  4. 加载(Load):页面的基础内容应当在 1 秒以内加载完成,并且应保持用户的输入响应速度。

由于 RAIl 这种分析方式十分直接且有针对性,所以被广泛应用于 Web 性能优化中。开发者可以根据 RAIl 性能模型来确定自己 Web 应用程序的瓶颈,从而能够深入研究性能问题的原因,为性能优化提供明确的路径。同时,在实际的应用过程中,也需要掌握一些有效的性能优化技巧和工具,从而达到最优化的效果。

参考链接:

developer.chrome.com/docs/devtoo…

www.electronjs.org/docs/latest…