如何处理大数据(~50 000 个对象)的 Vue 2 内存使用

新手上路,请多包涵

我正在尝试为 Vue 2 上的大型半复杂对象集合实现表视图。基本上,我的想法是从数据库中收集 50 000 到 100 000 行之间的任何行到 JS 缓存中,然后对其进行动态分析以构建表- 使用实时过滤器查看(文本搜索)。表中的每一行都是可切换的,这意味着单击该行会将行更改为编辑模式,从而为该特定字段/单元格启用类似 Excel 的编辑。

每个对象都有大约 100-150 个字段/属性,但在任何给定时刻表中只显示一定数量的字段/属性(表列可以实时切换)。对于大型数据集,DB 似乎正在推送大约 10-100mb 的 JSON 数据,这在这个用例中是可以接受的。 Renderwise 性能不是问题——过滤器工作得足够快并且只有有限数量的结果被渲染到 DOM。

一切都已经工作,过滤器,针对过滤器列出 ~100 行(+“显示 100 多个”机制等),但是当我将大约 8000 个对象加载到数组中时,我遇到了内存限制。这似乎保留了 2 GB 的 RAM,在 Chrome 停止运行 JS 代码之后(尽管奇怪的是我没有收到任何警告/错误)。

我对行的内存使用情况进行了基准测试,似乎 ~1000 行保留了大约 300mb 的内存。这很可能是由 Vue 反应观察者保留的。

三个问题:

  1. 有没有一种方法可以切换特定数组列表对象的反应性(通过索引等),以便数组本身内的对象是不可观察的/不可改变的,除非特别调用成为可变的(即当用户点击行时,这使得编辑 -模式)?
  2. 你将如何为 Vue 实现大型数据集的处理,因为反应性似乎是内存使用的瓶颈? _请不要建议“将结果限制在后端”,因为这不是我在这里寻求的解决方案_(即使我可能需要创建两部分过滤,一个用于获取较小的初始数据集,一个用于实时过滤)。基本上,我试图通过重新思考 Vue 的数据架构,将“内存结束”的界限从 8 000 推到 80 000。唯一的问题是将数据集存储在 Vue 的数据变量中作为反应式吗?
  3. 我的一个想法是使用 Object.freeze 或一些类似的方法将“项目”-数据集转换为不可观察/非反应性,并有表格来呈现两个数据集:一个用于非反应性数据集,一个用于当前在其中的数据集编辑模式(单击行时将被推送到“editableItems”数据集)…这里有任何建议(更简单的,以便我能够处理一个数组中的所有内容?)

我在 Angular 1 上完成了类似的应用程序,它可以很好地处理 50 000 行,所以我确信它在 Vue 2 中也应该可行……应该只是找到一种处理反应性的方法。

原文由 Janne 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

编辑 20.4.2021 - 两年后,两年更聪明

由于这个问题/答案得到了很多关注,并且在这么多年之后仍然有效,我想提出一些建议。下面的大部分细节仍然有效。尽管如此,在处理过滤结果和复杂对象时,我还是会直接使用 VueX 和 Lodash(或现代版本的原生 JS 函数)。

为了减轻后端的压力,您可以保持简单:获取没有相关模型的普通对象。这意味着您的主要结果只有相关对象的 ID 键。使用 Axios 或类似库通过单独的 AJAX 请求(“客户”、“项目”、“位置”)获取所有相关数据,并使用 VueX 将它们存储在自己的列表属性中。为每个创建吸气剂,例如:

 projectsById: state => {
    return _.keyBy(state.projects, "id")
},

这样,您可以在需要时使用相关模型来获取标签和/或完整对象, 并且 您的后端不需要多次获取相关数据。状态和吸气剂也将在微型组件中可用。

基本上:在处理大型数据集时,避免获取完整的模型树(即使 C# EF 或 PHP Laravel 为它们提供工具)。使用原子方法:获取 20 个不同的列表(“Axios.all([…])”是你的朋友!),每个列表都有自己的控制器端点并将结果缓存到 VueX 存储中……玩得开心;)

编辑 12.03.2019 - 此答案末尾的其他提示

我问这个问题已经有一段时间了,我终于开始优化我项目的这一部分了。我想为任何有这些性能和/或内存问题的人提供一些建议。

Vue 文档从未真正解释过它,但正如安德烈指出的那样,您可以将组件对象用作自定义对象和对象列表的数据存储。毕竟,它只是一个普通的 javascript 对象。

优化后我的列表组件设置看起来有点像这样:

 module.exports = {
    items: [],
    mixins: [sharedUtils],
    data: function() {
        return {
            columns: {
                all: []
    etc... Lot's of data & methods

items-array 充满了数以千计的复杂对象(大约 80mb 的数据,压缩的 6mb),我将其作为非反应性处理。事实证明,这比我想象的要小——我没有直接对项目使用 v-for,而是已经在使用结构,在该结构中,只要用户单击某个过滤器按钮和/或输入字符串,我就会触发对该数组的过滤——过滤(例如名称)。基本上这个“processFilters”方法通过非响应项目数组并返回存储在数据上下文中的过滤项目。因此,当它发生变异时,它会自动变成反应性的。

 <tr v-for="item in filteredItems"

这样,filteredItems 中的所有项目都保持反应性,但在被过滤掉时也会失去反应性,从而节省大量内存。高达 1200mb 缩小到 400mb,这正是我想要的。聪明的!

需要解决的问题很少。由于数据上下文中不存在项目,因此您不能直接在模板中使用它。这意味着不是写…

 <div v-if="items.length > 0 && everythingElseIsReady">

…我必须存储项目数组的长度来分隔数据道具。这也可以用计算值来解决,但我希望保留这些属性。

放弃主数据数组的反应性毕竟不是一件坏事 - 最重要的部分是了解直接针对该基本数组中的项目进行的修改永远不会触发对 UI 和/或的任何更改子组件 (douh)。这不应该是这样的问题,只要您以这样一种方式分离代码,即您拥有“隐藏数据容器”,其中包含来自后端的所有结果,并且您拥有该大型容器的较小(过滤)表示数组。通过使用良好的 REST 架构,您应该已经可以很好地使用非反应性数据存储,只要您记得在将项目保存在非反应性数据存储中后检查是否也已更新到最新版本。

此外,我感到困惑的是,相对于数百行,有多少微组件在性能方面无关紧要。渲染显然受到了影响,但即使我要传递大型道具数千次(因为我有数千个输入单元实例),它似乎也没有影响内存。这种对象之一是我的全局翻译键/值对对象,有超过 20000 行翻译字符串……但它仍然没有关系。这是有道理的,因为 Javascript 使用对象引用并且 Vue Core 似乎已正确编码,所以只要您使用诸如 props 之类的配置对象,您就只是从数千个对象引用同一个数据集。

最后,我要说的是,开始使用复杂的 CRUD 对象,而不用担心达到内存限制!

非常感谢 Andrey Popov 推动了正确的方向!

提示 (12.03.2019)

由于已经有一段时间了,并且随着我继续使用大型和复杂的数据集构建 UI,我决定放弃一些简短的想法和技巧。

  1. 考虑如何管理主记录(即人员或产品)与相关记录(子对象/关系对象)。尝试限制为子组件注入的数据量,因为您可能为不同的主记录多次表示同一个子对象。问题是这些对象可能实际上不是引用对象!

考虑一下你有 person-object,其中包含 city-object 的情况。多个人住在同一个城市,但是当您从后端获取 JSON 数据时,您确定那些重复的城市对象实际上是同一个城市(人与人之间共享/引用城市对象),还是类似对象的多个表示(与数据完全相同,但在幕后,每个数据都是一个单独的实例/唯一对象)。假设你有 50 000 个人,每个人都包含相同的子对象/属性“city”:{ id: 4, name: “Megatown” },你是不是只获取了 50 000 个单独的城市实例而不是一个?是 person1.city === person2.city ,还是它们看起来一样但仍然是两个不同的对象?

如果您不确定您是指共享城市对象还是使用许多类似子对象的实例,您可以简单地在您的人员列表组件中进行引用。您的人包含城市 ID,因此使用单独的 REST 方法 (getCities) 获取城市列表,并在 UI 级别进行配对。这样你只有一个城市列表,你可以从那个列表中解析城市并将其注入人,从而只引用一个城市。或者,您可以从列表中解析城市并将其作为属性传递给您的人员组件。

还要确保考虑子对象的目的是什么。您需要它是反应性的还是静态的?为了节省大量内存,你可以只告诉“person.city = city”,这将为每个人组件注入,但如果它需要反应,那么你需要使用 Vue.set -method .. . 并记住,如果每个城市都需要自己的实例(这样每个人都有相似的城市对象,但每个人都需要编辑属性),那么您需要确保您没有使用引用的对象!因此,您很可能需要克隆城市对象,这会耗尽浏览器内存。

  1. 您的微组件可能包含用于只读状态和编辑器状态的单独视图状态。这很常见。尽管如此,您实际上每次都在创建该微组件的实例,因此对该组件进行了数千次初始化。

想一想您拥有带有表格和表格行的类似 Excel 的电子表格的情况。每个单元格都包含您自定义的“我的输入”组件,它从您的布局中获取“只读”属性。如果 UI 处于只读状态,那么您将仅显示该 my-input-component 内的标签部分,否则您将显示具有某些特殊条件的 input-tag(例如日期时间、数字、文本具有不同的输入,文本区域、选择标签等)。现在假设您有 100 行 20 列,因此您实际上正在初始化 2000 个我的输入组件。现在的问题是——可以改进什么(性能方面)?

好吧,您可以将只读标签从我的输入组件分离到您的列表视图,这样您要么显示只读版本(标签),要么显示可编辑的我的输入组件。这样你就有了 v-if 条件,它确保这 2000 个微组件不会被初始化,除非你特别要求初始化它们(由于行或整个布局从只读 -> 可编辑状态移动)。 . 你可能猜到当 Vue 不需要创建 2000 个组件时,对浏览器的内存影响有多大。

如果你的页面加载速度非常慢,那可能根本就不是 VUE。检查呈现给 HTML 的 HTML 标记的数量。当您有大量标签时,HTML 的性能会很差。证明这一点的最简单方法之一是将带有 2000 个选项的 select-tag 重复 100 次,或者使用一个 20000 选项的 select-tag。同样的方式,你可能会因为有很多微组件和不必要的包装 div 等而溢出 html 标签的数量……你拥有的深度和标签越少,浏览器和 CPU 所需的渲染性能就越低。

尝试通过示例学习良好的 HTML 标记架构。例如,您可以研究如何对 Trello -services dashboard-view 进行编程。它是半复杂服务的非常简单和漂亮的表示,具有最少的 sub-div。


改进内存处理的方法有很多,但我认为最重要的方法与将“隐藏”对象与可见对象分开有关,如我最初的回答所述。第二部分是了解实例化对象与引用对象的区别。第三是限制对象之间不必要的数据传递量。

就我个人而言,我还没有尝试过这个,但是存在一个 Vue-virtual-scroller 组件,它通过简单地作为看似无限量数据的包装器来处理任何数量的数据。查看概念 @ https://github.com/Akryum/vue-virtual-scroller ,让我知道它是否为您解决了问题。

我希望这些指南能为优化您的组件提供一些想法。永远不要放弃希望,总有改进的余地!

原文由 Janne 发布,翻译遵循 CC BY-SA 4.0 许可协议

从我读过的所有内容中,我发现您不需要对该数据进行反应,因为:

表中的每一行都是可切换的,这意味着单击该行会将行更改为编辑模式,从而为该特定字段/单元格启用类似 Excel 的编辑

这意味着行不可编辑,并且在没有用户交互的情况下无法更改数据。

每个对象都有大约 100-150 个字段/属性,但在任何给定时刻表中只显示一定数量的字段/属性(表列可以实时切换)。

您保持字段反应但不显示它们。


现在你的问题

有没有一种方法可以切换特定数组列表对象的反应性(通过索引等),以便数组本身内的对象是不可观察的/不可改变的,除非特别调用成为可变的(即当用户点击行时,这使得编辑 -模式)?

如果一次可以编辑一个项目,那么为什么要让所有内容都处于反应状态?您可以轻松地使用单个变量来监听该更改。

你将如何为 Vue 实现大型数据集的处理,因为反应性似乎是内存使用的瓶颈?

这一切都与实施有关——您很少会遇到需要大量项目才能做出反应的情况。您拥有的项目越多,为了使用反应性需要发生的事件就越多。如果你有 50k 个项目并且只有几个事件要改变(比如用户手动修改数据),那么你可以轻松地监听 这些 事件并手动进行反应,而不是让 Vue 处理所有数据。您可以查看 Vuex ,它可以让您的生活更轻松一些 :)

我的一个想法是使用 Object.freeze 或一些类似的方法将“项目”-数据集转换为不可观察/非反应性,并有表格来呈现两个数据集:一个用于非反应性数据集,一个用于当前在其中的数据集编辑模式(单击行时将被推送到“editableItems”数据集)

这是朝着正确的方向前进,但不需要支持两个数组。想象一下使用这样的东西:

 data: function() {
    return {
        editingItem: {}
        // when editing is enabled bind the input fields to this item
    }
},
created: function() {
    this.items = [] // your items, can be used in markdown in the loop, but won't be reactive!
},
watch: {
    editingItem: function(data) {
        // this method will be called whenever user edits the input fields
        // here you can do whatever you want
        // like get item's id, find it in the array and update it's properties
        // something like manual reactivity ;)
    }
}

原文由 Andrey Popov 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题