对象数量与有效负载,同时扩展现代 Javascript 项目哪个更重要?
Posted
技术标签:
【中文标题】对象数量与有效负载,同时扩展现代 Javascript 项目哪个更重要?【英文标题】:Number of objects vs Payload, while scaling a modern Javascript project which is more important? 【发布时间】:2018-01-31 00:56:13 【问题描述】:当然,更少的有效负载等于更少的对象数量,但请阅读下面的完整描述。
在现代浏览器上扩展 javascript 项目时,哪个更重要?数据负载的大小或内存中 javascript 对象的数量。我有一个巨大的 JSON 字符串,我正在循环并将那个巨大的字符串切割成不同的对象。 JSON 字符串包含很多旅行者信息,每个 Javascript 对象都有很多属性。当 JSON 中有超过 10,000 名旅行者时,浏览器正在努力执行。
如果我可以减少属性的数量,我会带来很多不必要的属性,显然我的有效负载会减少,但对象的数量可能会保持不变。
大量的 JS 对象与更小的有效负载,哪一个在性能方面更划算?
谢谢
【问题讨论】:
这 10,000 名旅行者是您数据库中的所有数据吗?为什么要一次处理所有这些?似乎其中一些内容可能更适合后端,或者您应该向前端发送更少的信息。 如果您仍在循环遍历所有对象,我认为属性较少的对象并不重要。我认为应该有更多关于您如何具体处理数据的数据和信息,以更好地调查效率问题。 您好@nbkhope,我们的数据库中有数百万旅客。有时我们带来超过 10k 行,然后浏览器似乎滞后。如果检索到的数据较低,则应用程序运行良好且流畅。 可能感兴趣JSON.parse() on a large array of objects is using way more memory than it should 这完全取决于您对数据的实际操作,以及瓶颈在哪里。 JS 很快,10k 个对象并不多,JS 会很好地处理它/操纵它。但是如果你说你正在解析一个 10MB 的 json 字符串,它构建了 10k 个对象,那么是的,这会很慢。 【参考方案1】:我喜欢我读过的一些答案,但我想我会以不同的方式解决瓶颈问题,我希望您也可以通过这种方式避免未来的瓶颈。
大多数答案都假设对象的数量是瓶颈。我认为情况并非如此。我认为瓶颈在于 JavaScript 事件循环过度积压。
众所周知,JavaScript 只运行一个线程和一个事件循环。
您调用的每个函数实际上都是此类事件的回调。
但是,由于只有一个线程,网页中的任何代码都必须等待每个事件完成,然后才能执行任何其他任务。
这意味着将 JavaScript 函数分段(成微事件/回调)比任何单个函数以性能为导向更重要。
在您的情况下,您既要循环一个长字符串并执行操作 - 没有将控件返回到事件循环 - 这意味着浏览器必须等待这一大块代码在它可以处理更多数据/事件之前完成。
数据收集/处理的问题可以争论。获取大量小消息是否更好(可能会在网络/服务器上放置更多负载)?接收一个巨大的字符串并按块处理它会更好吗? ...
...我不知道,这确实取决于其他因素,例如服务器的设计,数据库的设计,客户端负载,更新间隔等。
如果您确实更喜欢处理单个大字符串,最好一次处理一点,然后将其转发给回调以供将来处理。
即对于\n
分隔的 JSON 字符串,您可以尝试:
function consumeString(s)
if(s.length == 0)
return;
var sep = s.indexOf("\n");
if(sep < 0)
sep = s.length;
try
var obj = JSON.parse(s.slice(0, sep));
console.log("processed:", obj);
catch
console.log("Failed... not valid JSON?:");
// schedule the next slice for later processing.
setTimeout(consumeString, 0, s.slice(sep + 1));
var text = ' "employees" : [' + // JSON1
' "firstName":"John 1" , "lastName":"Doe 1" ,' +
' "firstName":"Anna 1" , "lastName":"Smith 1" ,' +
' "firstName":"Peter 1" , "lastName":"Jones 1" ]' + // END JSON1
"\n" +
' "employees" : [' + // JSON2
' "firstName":"John 2" , "lastName":"Doe 2" ,' +
' "firstName":"Anna 2" , "lastName":"Smith 2" ,' +
' "firstName":"Peter 2" , "lastName":"Jones 2" ]';
consumeString(text);
显然,这只是一个大纲,但尽管它似乎性能较差(它浪费时间重新调度自身并且不断中断,增加了 CPU 缓存未命中的机会)......它实际上有助于浏览器保持响应并改善感知从用户的角度来看性能。
【讨论】:
【参考方案2】:10,000 个对象对于现代 JavaScript VM 来说似乎并不多。您的问题似乎源于您在内存中处理大量字符串和更新 DOM,这两者都是缓慢且占用大量内存的操作。
请注意Strings are immutable in JavaScript,因此每次您切碎它时,您都会在内存中创建它的新实例。如果您在循环内进行字符串操作,这尤其糟糕。 每次迭代都会导致在内存中创建一个新的 String 对象。旧的现在已被丢弃,但可能不会立即收集垃圾,因此会占用内存。如果您正在处理大块字符串,问题会变得更糟。 Javascript 不适合执行这种类型的长时间运行的内存操作,因为它是single threaded。而且这样的操作会占用主线程,从而使您的应用程序变慢。
while(...)
// this will create a new instance of fullStr in memory every iteration
fullStr = fullStr + 'str1';
如果操作不正确,即使使用像 React 这样的现代 JS 库,DOM 操作也会非常缓慢。如果您通过将新节点附加到现有 DOM 来继续呈现新行,您将看到性能问题。 DOM 操作会导致重排和重绘,这两者都非常slow 和计算密集型。每次调用render
都可能导致完全回流和重绘。如果您的高阶组件在 prop 更改时渲染,它可能会强制所有子组件重新渲染,这会减慢您的页面速度。 React 确实通过批处理更新解决了其中一些问题。
很多人没有考虑到的另一件事是对mousemove
或scroll
等事件使用事件处理程序的副作用。每一个微小的移动都可能导致页面上的元素以某种方式发生变化,这也可能导致 DOM 其他部分的重绘或重排。在页面上使用大量动画时,情况会变得更糟。这些类型的事件处理程序还可以通过不断触发来占用 VM 事件循环。解决这个问题的方法是使用throttling。
最不可能但需要考虑的一点是,对象在创建时会产生额外的保持引用计数的开销。对我们来说,VM 似乎正在将一个对象及其所有属性和值存储在一个连续的内存块中。但是,在reality 中,对象存储对存储在内存中其他位置的实际值的引用。对于大量对象,这种开销可能是不小的。
有一种误解,认为创建 class
或 function
的新实例会导致大量内存开销,因为所有属性和值都被克隆。这是不正确的。 JavaScript 对prototypes
的使用通过与特定类或函数的所有实例共享相同的属性来减少内存使用。这就是为什么原型继承在内存使用方面非常有效的原因。不过,较深的原型链可能会导致查找缓慢。
【讨论】:
【参考方案3】:考虑到我的专业领域只是跨越了这个问题的范围而不是完全涵盖它,我会给出我能给出的最佳答案。
现代计算机有一个内存层次结构:主内存;二级或三级缓存。每一级缓存都比它上面的缓存更小,但更接近 CPU(实际上速度更快)。
因此,就程序运行的速度而言,与当前的“工作集”(程序同时使用的内存部分)相比,它使用了多少总内存并不重要瞬间。
根据您的描述,听起来将大 JSON 字符串分解为许多较小的对象将有助于在大多数情况下减少工作集的大小。如果在执行此操作时留下了许多未使用的属性,那可能并不重要,因为它们可能大部分时间都留在工作集之外。
让后端过滤掉一些不需要的属性可能是一种有用的技术,因为这会减少传输的数据量。但是,您需要仔细考虑这将如何扩展;它可能会给服务器带来过多的负载。
可能有一些方法可以通过重新评估您编写的算法和您使用的库(方法)来加速您的程序。可能有一些特定于典型现代 JavaScript 引擎的技术;恐怕这超出了我的专业范围。
在您的应用程序中引入一些新概念可能会有所帮助。通过采用启发式(系统猜测)或降低准确性来减少处理是否可以接受?
例如,可以根据仅使用部分数据的第一轮向用户显示临时结果。微调器(或消息)可用于提醒用户正在进行更多处理。处理完完整的数据集后,将其呈现给用户。
或者,让用户选择“快速”或“深度”结果;深层结果可能会在后端进行计算(并且可能会将其外包给 HTTP 或数据库服务器以外的机器)。
HTH 祝你好运!
【讨论】:
【参考方案4】:JavaScript 对象的数量很可能是您方案中的瓶颈。您可以进行一些简单的控制,例如检查操作系统是否进入交换状态。下面我粘贴了一个 MacOs 活动监视器屏幕截图,您可以在其中检查交换使用情况。 Linux 和 Windows 操作系统也存在相同的信息。
另一种控制方法可以是,检查您对数据进行的过滤/映射/减少/每个操作的数量。如果你做了很多这样的操作,它会增加你的代码被对象数量卡住的可能性。尝试减少对象的数量(您已经知道)或减少重复迭代对象的需要(例如,如果您一遍又一遍地找到相同的信息,请将其存储到中间对象)。
【讨论】:
【参考方案5】:对象中的每个属性本身就是一个对象。 Javascript 那样就很有趣。
例子
myObject
prop1: 'hello',
这里有一个“对象”myObject
,它继承自 Object
原型,并带有对象的所有开销。该对象包含一个属性prop1
,它是一个字符串。一个字符串继承自String
,后者继承自Object
。因此,您的字符串也带有对象的所有开销。因此,在内存使用方面,拥有更多属性应该与拥有更多对象大致相同。
如果我可以减少属性的数量,我会带来很多不必要的属性,显然我的有效负载会减少,但对象的数量可能会保持不变。
大量的 JS 对象与更小的有效负载,哪一个在性能方面更划算?
我不确定你在这里的确切意思,但听起来你是在说你肯定需要所有 10,000 名旅客,但如果每个旅客可能有 20 处房产而不是 100 处,你会很好。
是的!去做。更少的数据意味着在内存中跟踪的更少,更小的堆栈可以挖掘以再次找到您的数据等等。10000 * 20 比 10000 * 100 在这里要好得多。
但是,我仍然鼓励您重新考虑将这么多数据点带入浏览器是否有意义。我能想到的用例并不多,你甚至可以在哪里展示它。最好让您的服务器处理更多数据,然后只发送您实际可以在屏幕上显示的小子集。
服务器可以将其 cpu 时间用于将数据转换为有用的小块,而浏览器则将其 cpu 时间用于使用户体验如丝般流畅。
【讨论】:
您的陈述“它继承自对象原型并带有对象的所有开销”,这不是正确的陈述。使用prototype
的美妙之处在于,只有一个属性实例与所有实例共享。您所有的对象或字符串将共享相同的原型链,因此不会导致内存膨胀。可能发生的一件事是,随着原型链变长,属性查找会变慢。这是 JavaScript 继承的基础。
是的。这就是原型继承的美妙之处。 “一个对象的所有开销”根本不是太多开销。但这并没有改变我的主要观点,即每个属性与它所附加的父对象的权重大致相等。如果您觉得我没有准确描述这一点,请随时进行编辑,只要您不丢失这一点。如果您需要通过同行评审,可以在编辑原因中引用此评论。【参考方案6】:
您可以尝试将数据写入 IndexedDB,然后查询您实际需要的数据,我怀疑您是否需要一次显示所有 10k 个对象。这样您就不会在内存中保存大量数据,但您仍然可以访问它。看看Dexie.js 它有非常简单的api。也许值得一试。
【讨论】:
以上是关于对象数量与有效负载,同时扩展现代 Javascript 项目哪个更重要?的主要内容,如果未能解决你的问题,请参考以下文章