前端开发人员应该关心性能吗?
我最近和亚马逊的一位架构师聊天,他跟我提了一个很有意思的问题。我们当时讨论的是某个算法的复杂度(用大O符号来讨论),还没等我们深入解释,他就说:
我的意思是,我们不需要太担心这个。毕竟我们是前端开发者!
我觉得这番坦白令人耳目一新,而且这番话出自亚马逊这个象牙塔里的人之口,完全出乎意料。我一直都知道这件事。不过,听到这番话出自一个在 FAANG 这样的公司工作的人之口,我还是很高兴。
你看,性能是程序员们最痴迷的话题之一。他们把它当成一种荣誉勋章。他们看到你使用了 JavaScript 的原生.sort()
方法,就会嗤之以鼻,说些“嗯,你知道……这增加了O(n log(n))
复杂性”之类的话。然后他们得意洋洋地走开,仿佛把你的代码扔进了“失败算法”的垃圾箱。
智能客户端与哑终端
近几十年来,“智能客户端”和“哑终端”这两个术语已经逐渐被人们淡忘。但即使在现代计算环境中,它们仍然是有效的定义。
大型机计算
早在黑暗时代,几乎所有的计算都是在大型计算机(例如大型机)上完成的。你通过“终端”与这些计算机交互。这些终端通常被称为“哑终端”,因为终端本身几乎没有任何计算能力。它只是一种向大型机发送命令,然后查看大型机返回结果的方式。这就是为什么它被称为“哑终端”。因为终端本身实际上无法独立完成很多事情。它只是一个让你访问大型机的门户。
对于编写大型机代码的人来说,他们不得不非常担心算法的效率。因为即使是大型机,其计算能力也相对有限(以今天的标准来看)。更重要的是,大型机的资源由任何能够访问哑终端的人共享。因此,如果100个人坐在100台哑终端前,同时发送资源密集型命令,很容易导致大型机崩溃。(这也是为什么终端分配非常严格,即使是那些能够使用大型机终端的人也常常需要预留使用时间的原因。)
个人电脑计算
随着80年代个人电脑的爆炸式增长,突然间,许多人拥有了强大的计算能力(相对而言)。然而,大多数时候,这些计算能力并未得到充分利用。由此,“智能客户端”时代应运而生。
在智能客户端模型中,我们会尽一切努力让客户端自行进行计算。只有当需要从数据源检索现有数据,或需要将新数据/更新数据发送回数据源时,客户端才会与服务器进行通信。这将大量工作从大型机转移到客户端,从而有助于创建更强大的应用程序。
回归大型机计算(有点......)
但随着网络的出现,许多应用程序又回到了服务器/终端的关系。这是因为这些应用程序看起来像是在浏览器中运行,但事实是,早期的浏览器技术本身无法真正完成很多工作。早期的浏览器就像哑终端一样。它们可以查看从服务器发送的数据(以 HTML/CSS 的形式)。但如果它们想以任何有意义的方式与这些数据进行交互,就需要不断地将命令发送回服务器。
这也意味着早期的 Web 开发者需要高度重视效率。因为如果你的网站突然爆红,数百(甚至数千)个用户同时运行这段代码,即使是一段看似无害的代码,也可能让你的服务器崩溃。
部署更强大的后端技术可以在一定程度上缓解这种情况。例如,您可以部署一个 Web Farm来分担单个站点的请求负载。或者,您可以使用编译型语言(例如 Java 或 C#)编写代码,这(在一定程度上)有所帮助,因为编译型代码通常比解释型代码运行速度更快。但是,您仍然会受到所有公共用户访问有限服务器/计算资源的限制。
浏览器即智能客户端
我不会深入探讨 Chrome 的众多利弊。但它对 Web 开发最大的贡献之一是,它是首批持续优化 JavaScript 性能的浏览器之一。当这种优化与 jQuery(后来是 Angular、React 等等)等强大的新框架相结合时,它促进了前端开发人员的崛起。
这不仅赋予了我们新的前端功能,也意味着我们可以重新思考桌面(浏览器)如何成为一个智能客户端。换句话说,我们不必彻夜难眠地担心一行异常代码是否会导致服务器崩溃。最坏的情况是,它只会导致某人的浏览器崩溃。(别误会,编写导致浏览器崩溃的代码仍然是一件非常糟糕的事情。但当桌面/浏览器通常拥有大量未使用的 CPU 周期等待利用时,这种情况发生的可能性就小得多。)
那么,当你在编写下一个伟大的 React 应用时,你究竟需要在多大程度上关注性能呢?毕竟,你的应用的大部分功能都将在用户的浏览器中运行。即使该浏览器是在移动设备上运行,它也可能拥有大量未充分利用的处理能力供你使用。那么,你需要在多大程度上关注代码性能的这些细节呢?在我看来,答案很简单,但却很微妙。
在乎……但不要太在乎
几年前,我听过一位上市公司首席执行官的主题演讲。上市公司必须时刻关注股市(这可以理解)。在演讲中,他提出了一个问题:我有多关心公司股价?他的回答是,他关心……但没那么关心。换句话说,他始终关注股价。当然,他也清楚公司可以做(或避免)哪些事情可能会影响股价。但他坚持认为,他不能把所有公司内部决策都仅仅基于一个简单的因素——是否会推高股价。他必须关注股价,因为股价暴跌会给上市公司带来各种各样的问题。但如果他只顾眼前利益,只关注股价,最终可能会做出一些只会让股价上涨几美分,但最终损害公司利益的决策。
在我看来,前端应用开发非常相似。你应该时刻关注代码的性能。你当然不想写出导致应用运行明显糟糕的代码。但你也不想在每个开发周期中都花费一半的时间来优化代码的每一个细节。
如果这一切听起来非常抽象,我会尝试为您提供一些指导,告诉您何时需要关心应用程序性能 - 以及何时不应该让它阻碍您的开发。
开发者试用
首先要记住的是,你的代码(但愿如此)会被其他开发者审核。无论是你提交新代码,还是几个月后有人来查看你写的代码,都会有审核。而且很多开发者喜欢为了性能而挑剔你的代码。
你无法避免这些“试验”。它们无时无刻不在发生。关键在于不要陷入关于循环基准性能for
与Array.prototype
函数的理论争论.forEach()
。相反,你应该尽可能地将话题引回到现实的领域。
基于现实的基准测试
我所说的“现实”是什么意思呢?首先,我们现在有很多工具可以在浏览器中对我们的应用进行基准测试。所以,如果有人能指出,我可以通过一两处小改动来缩短应用的加载时间,我洗耳恭听。但如果他们提出的优化方案只能“节省”我几微秒,我可能会忽略他们的建议。
您还应该意识到,语言的内置函数几乎总是比任何自定义代码的性能更佳。因此,如果有人声称他们有一些自定义代码比 (例如 ) 更高效,我会立即表示怀疑。但如果他们能向我展示如何在完全不使用 的Array.prototype.find()
情况下实现预期结果,我会很乐意听取他们的建议。然而,如果他们只是简单地认为他们实现 (例如 ) 的方法比使用 (例如 ) 更高效,那么我就会非常怀疑。 Array.prototype.find()
.find()
Array.prototype.find()
代码的运行时环境
“现实”也由一个简单的问题驱动:代码在哪里运行?如果有问题的代码运行在 Node 中(也就是说它在服务器上运行),性能调整就会变得更加紧迫,因为这些代码是共享的,每个使用该应用的人都会用到。但如果代码运行在浏览器中,即使你没有优先考虑性能调整,你也不会被认为是一个糟糕的开发者。
有时,我们正在检查的代码甚至根本没有在应用程序中运行。每当我们决定进行纯粹的学术练习,以评估我们对性能指标的整体认识时,就会发生这种情况。像这样的代码可能在 JSPerf 面板中运行,或者在 StackBlitz 上编写的演示应用程序中运行。在这些情况下,人们更有可能关注性能的有限细节,仅仅因为这是练习的全部意义所在。正如你可能想象的那样,这类讨论往往最常出现在……求职面试中。因此,当观众真正关心的几乎只有性能时,对性能完全不屑一顾是很危险的。
数据类型的“权重”
“现实”还应该包括彻底理解你正在操作的数据类型。例如,如果你需要对一个数组进行大规模转换,那么你完全可以问自己:这个数组合理可以变成多大?或者……这个数组通常可以存储哪些类型的数据?
如果您有一个仅包含整数的数组,并且我们知道该数组永远不会包含超过十几个值,那么我真的不太关心您选择用于转换该数据的具体方法。您可以使用.reduce()
嵌套在 a 内部.find()
,嵌套在 a 内部.sort()
,最终从 a 返回.map()
。您知道吗?无论您选择在什么环境中运行该代码,它都能正常运行。但如果您的数组可以保存任何类型的数据(例如,包含嵌套数组的对象、包含更多对象的对象、包含函数的对象),并且如果可以想象该数据几乎具有任意大小,那么您需要更加仔细地考虑用于转换它的深度嵌套逻辑。
大O符号
对我来说,性能方面一个特别棘手的问题就是大 O 符号。如果你拥有计算机科学学位,你可能必须非常熟悉大 O 符号。如果你是自学的(像我一样),你可能会觉得它……很繁琐。因为它很抽象,而且通常对你的日常编程任务没有任何帮助。但如果你想通过大型科技公司的编程面试,它可能会在某个时候出现。那么你该怎么办呢?
好吧,如果你想给那些痴迷于大O符号的面试官留下深刻印象,那么你可能别无选择,只能埋头苦干,强迫自己学习它。不过,有一些捷径可以帮助你轻松熟悉这些概念。
首先,了解一些非常简单的基础知识:
-
O(1)
是你能得到的最直接的时间复杂度。如果你只是设置一个变量,然后在稍后的某个时间点访问该变量的值,这就是O(1)
。这基本上意味着你可以立即访问存储在内存中的值。 -
O(n)
是一个循环。n
表示需要遍历循环的次数。因此,如果您只创建一个循环,那么您编写的代码就比较O(n)
复杂。此外,如果一个循环嵌套在另一个循环中,并且两个循环都依赖于同一个变量,那么您的算法通常是O(n-squared)
。 -
我们使用的大多数“内置”排序机制都比较复杂。排序的方法
O(n log(n))
有很多种O(n log(n))
。但通常情况下,当你使用某种语言的“原生”排序函数时,你就会运用复杂性。
你可以深入钻研,试图掌握大 O 符号中的所有“边缘情况”。但如果你理解了这些极其简单的概念,你至少已经能够在大 O 符号的对话中站稳脚跟了。
其次,你不一定需要“知道”大 O 符号才能理解这些概念。这是因为大 O 本质上是一种简写形式,用来解释“我的代码需要经过多少个循环才能完成计算”。
例如:
const myBigHairyArray = [... thousandsUponThousandsOfValues];
const newArray = myBigHairyArray.map(item => {
// tranformation logic here
});
这种逻辑很少出问题。因为即使myBigHairyArray
数组非常大,你也只需要循环一次。而且现代浏览器可以非常快速地循环遍历数组——即使是一个很大的数组。
但是如果你想写这样的内容,你应该立即开始思考你的方法:
const myBigHairyArray = [... thousandsUponThousandsOfValues];
const newArray = myBigHairyArray.map(outerItem => {
return myBigHairyArray.map(innerItem => {
// do inner tranformation logic
// comparing outerItem to innerItem
});
});
这是一个嵌套循环。需要明确的是,有时嵌套循环是绝对必要的,但选择这种方法时,时间复杂度会呈指数级增长。在上面的例子中,如果myBigHairArray
“仅”包含 1,000 个值,则逻辑需要对它们进行一百万次迭代(1,000 x 1,000)。
一般来说,即使你对大 O 符号最简单的概念一无所知,也应该始终努力避免嵌套任何内容。当然,有时嵌套是不可避免的。但你应该始终仔细思考是否有办法避免。
隐藏循环
您还应该注意使用原生函数时可能出现的“陷阱”。没错,原生函数通常是一件“好”事。但是,当您使用原生函数时,很容易忘记其中许多函数在幕后通过循环发挥着神奇的作用。
例如:想象一下,在上面的例子中,您正在使用.reduce()
。使用 本身并没有“错误” .reduce()
。但.reduce()
它也是一个循环。因此,如果您的代码似乎只使用了一个顶级循环,但.reduce()
在该循环的每次迭代中都有一个发生,那么您实际上是在用嵌套循环编写逻辑。
可读性/可维护性
性能讨论的问题在于,它们往往侧重于微优化,而牺牲了可读性/可维护性。而我坚信,可维护性几乎总是比性能更重要。
我当时在城里一家大型健康保险公司工作,我编写了一个函数,该函数必须对大型数据集进行一些复杂的转换。当我完成代码的第一遍时,它可以工作。但是它相当……迟钝。因此,在提交代码之前,我对其进行了重构,以便在中间步骤中将数据集保存到不同的临时变量中。这种方法的目的是向任何阅读代码的人说明此时数据发生了什么。换句话说,我编写的是自文档化的代码。通过为每个临时变量分配不言自明的名称,我让所有未来的程序员清楚地知道每一步之后到底发生了什么。
当我提交拉取请求时,开发经理(顺便说一句,他是个十足的白痴)告诉我把所有临时变量都删掉。他的“逻辑”是,这些临时变量各自代表着不必要的内存分配。你知道吗?他并没有“错”。但他的做法很无知。因为这些临时变量对用户来说根本没什么区别,但它们会让以后的代码维护变得非常容易。你可能已经猜到我没在那份工作上待太久了。
如果您的微优化实际上使其他程序员更难理解代码,那么这几乎总是一个糟糕的选择。
该怎么办?
我可以自信地告诉你,性能是你应该考虑的问题。几乎是持续的,即使是在前端应用上。但你也需要现实地认识到,你的代码几乎总是运行在有大量未使用资源的环境中。你还应该记住,最“高效”的算法并不总是“最佳”的算法,尤其是当它对所有未来的程序员来说都像官样文章时。
思考代码性能是一项很有价值的练习。任何一位认真的程序员都应该时刻铭记于心。不断挑战自己(以及他人)对代码性能的相对要求是非常有益的。这样做可以极大地提升你的技能。但性能永远不应该是你工作的全部。如果你是一名“前端开发者”,这一点尤其重要。
文章来源:https://dev.to/bytebodger/should-frontend-devs-care-about-performance-3eg1