acm-header
登录

ACM通信

实践

JavaScript和Netflix用户界面


JavaScript的标志

信贷:Gaurang

回到顶部

在它被引入之后的20年里,JavaScript已经成为事实上的Web官方语言。就运行时环境的数量而言,JavaScript胜过其他所有语言。如今市场上几乎每一个消费硬件设备都以某种方式支持该语言。虽然这通常是通过集成Web浏览器应用程序来实现的,但现在许多设备也将Web视图作为操作系统用户界面(UI)的一部分原生支持。例如,在大多数平台(手机、平板电脑、电视、游戏机)上,Netflix的用户界面几乎完全是用JavaScript编写的。

尽管作为一种旨在成为Java“愚蠢的小兄弟”的语言,它的起点并不高,4JavaScript最终成为实现Web 2.0发展的关键组件。通过引入Ajax,这种发展为Web添加了一个动态元素,创造了一个现在被认为是理所当然的动态和社交Web的概念。今天,随着它通过Node.js进入服务器领域,语言的影响力继续增长。尽管有这些缺点,但可以说JavaScript已经成功地实现了“一次编写,随处运行”的座右铭,这是Sun Microsystems经常吹捧的Java的优点之一。

随着越来越多的应用程序逻辑转移到浏览器,开发人员已经开始突破JavaScript最初的目的。现在,整个桌面应用程序都在用javascript重新构建——谷歌Docs办公套件就是一个例子。这样的大型应用程序需要创造性的解决方案来管理加载所需JavaScript文件及其依赖项的复杂性。当引入多元A/B测试(这是Netflix DNA的核心概念)时,问题可能会变得更加复杂。多元测试引入了许多JavaScript无法使用本机构造处理的问题,其中之一就是本文的重点:管理条件依赖性。尽管如此,工程上的独创性已经使Netflix能够以一种快速和可维护的方式构建高度复杂和迷人的ui。

回到顶部

A / B测试Netflix.Com

Netflix沉浸在a /B测试的文化中。该服务的所有元素,从电影个性化算法到视频编码,一直到UI,都是A/B测试的潜在目标。通常会发现典型的Netflix用户同时被分配到30到50个不同的A/B测试。以这种规模运行测试提供了同时尝试全新方法和多种演进方法的灵活性。这一点在UI中表现得最为明显。

虽然许多A/B测试是在多个平台和设备上同步发布的,但它们也可以针对特定设备(手机或平板电脑)。这些测试允许对不同订阅者进行截然不同的UI体验的试验,这些测试的活动生命周期可以从一天到六个月甚至更长。我们的目标是了解这些设计背后的核心理念的根本差异如何使Netflix提供更好的用户体验。

Netflix网站上的A/B测试倾向于添加新功能或改变现有功能以增强控制体验。许多网站测试从一开始就设计为跨分配友好的,换句话说,可以与其他A/B测试叠加。这确保了新引入的功能可以与其他测试共存。因此,虽然一个Netflix订阅者的主页在表面上看起来与另一个订阅者的主页相似,但在整个页面中添加或修改各种各样的功能,以使最终产品感觉不同。需要指出的是,测试包括Netflix UI的所有部分(HTML、CSS和JavaScript),但这里的重点是使用JavaScript来减少问题的范围。

回到顶部

从facet到特性到模块

HTTP Archive估计,2014年平均每个网站在18个不同的文件中包含了大约290KB的JavaScript。2相比之下,Netflix今天的主页在一个JavaScript文件中平均提供了150KB的负载。这个文件实际上由30到50个连接在一起的不同文件组成,它们在有效负载中的包含由Netflix的推荐算法生成的数百个个性化方面中的一个或多个决定。这些方面通常可以通过订阅者的a /B测试分配、注册国家、观看品味和分享偏好(Facebook整合)得出,但可以被任何任意逻辑支持。这些方面充当开关,通过这种方法UI可以有效地旋转和调整。这使得网站陷入了一个非常独特的困境:如何以可维护和高性能的方式管理许多不同ui的打包和交付。

在个性化方面和它们对UI的影响之间画一条清晰的线是很有用的。一个简单的例子可以帮助说明这种关系。假设今天我们想用A/B测试一个搜索框。对于这个测试,我们可能有一个控制单元格,这是将用户发送到搜索结果页面的传统体验。为了适应用户体验的区域差异,我们还根据用户是否位于美国,对控制单元进行了轻微的变化。第一个测试单元提供自动完成功能,并可用于单元1中分配的所有用户。此场景中的分配意味着随机选择订阅者参与此测试。第二个测试单元格通过将结果显示为用户类型,在当前页面右侧提供搜索结果。我们叫它即时搜索,它对单元2中分配的所有订阅者可用。这是三个不同的体验,或“功能”,每一个都是由一组非常具体的个性化方面。因此,当用户被分配到测试时,当他们的facet满足测试的需求时,用户只会看到其中一种搜索体验(参见表1).页面的其他部分,如页眉或页脚,可以以类似的方式进行测试,而不会影响搜索框测试。

在这种测试策略下,有必要将网站的每个功能部分分离成离散的沙盒文件,称为模块.在JavaScript中,模块已经成为一种常见的最佳实践,可以将相关特性安全地分组在一个离散的、内聚的单元中。由于各种技术原因,这是可取的:它减少了对隐含全局变量的依赖;它允许使用私有/公共方法和属性;它允许真正的进口/出口系统的存在。导入/导出也为正确的依赖项管理打开了大门。

在这种情况下,模块背后还有另一个驱动力。它们允许功能从一个页面无缝移植到下一个页面。应该将Web页面分成越来越小的部分,直到可以使用现有模块组合新的有效负载。如果必须从以前的模块中打破功能来实现这一点,这可能表明有问题的模块承担了太多的责任。单元越小,维护、测试和部署就越容易。

最后,使用模块封装特性提供了在限制A/B测试的个性化方面之上构建抽象层的能力。由于可以将测试的合格性映射到特定的特性,然后将特性映射到模块,因此可以为给定的订阅者有效地解析JavaScript有效负载,只需确定订阅者对当前活动的每个测试的合格性。

回到顶部

依赖关系管理

模块还允许更高级的技术发挥作用,其中一个对复杂应用程序至关重要:依赖关系管理。在许多语言中,依赖项可以同步导入,因为运行时环境与请求的依赖项位于同一台机器上。然而,管理浏览器端JavaScript依赖项的复杂性在于,运行时环境(浏览器)与其源(服务器)之间存在不确定的延迟量。网络延迟可以说是当今Web应用程序性能中最重要的瓶颈,1因此,挑战在于找到带宽和延迟之间的平衡,对于一组给定的不确定约束,每个订户、每个请求都可能不同。


Netflix沉浸在a /B测试的文化中。该服务的所有元素,从电影个性化算法到视频编码,一直到UI,都是A/B测试的潜在目标。


多年来,Web社区设计了几种方法来处理这种复杂性,并取得了不同程度的成功。早期的解决方案只是包含页面上的所有依赖项,而不管是否会使用模块。虽然简单且一致,但带宽限制往往会加剧本已很长的加载时间,从而全面惩罚用户。后来的解决方案依赖于浏览器在确定缺少依赖项时向服务器发出多个异步请求。这也有它的缺点,因为它不利于深度依赖树。在这个实现中,一个带有依赖树的有效负载N节点深度可能会达到N- 1个串行请求在所有依赖被加载之前。

最近,异步模块定义(AMD)库的引入,比如RequireJS,允许用户创建模块,然后通过静态分析依赖树,在每个页面上预先生成有效负载。该解决方案结合了前面两种解决方案的优点,生成了只包含页面所需内容的特定负载,并避免了基于依赖树深度的不必要惩罚。更有趣的是,用户还可以选择完全不使用静态分析步骤,转而使用依赖项的异步检索,或者他们可以两者结合使用。在图1,一个名为喷火有三个附件。因为depC异步获取,N-在网页准备就绪前提出1次额外请求(其中包括N= 2,N是树的深度)。应用程序的依赖树可以使用静态分析工具来构建。

回到顶部

有条件的依赖关系

AMD和类似的解决方案的问题是他们假设有一个静态依赖树。在运行时环境与源代码并存的情况下,通常导入所有可能的依赖项,但只执行一个代码路径,这取决于上下文。不幸的是,在浏览器中这样做的后果要严重得多,尤其是在规模上。

通过回忆之前的搜索框A/B测试,可以更好地可视化问题,该测试有三种不同的搜索体验。如果页面标题依赖于搜索框,如何加载只有正确的搜索框体验给用户?可以将它们全部添加到有效负载中,然后让父模块添加允许其确定正确操作过程的逻辑(参见图2).然而,这是不可扩展的,因为它将A/B测试特性的知识注入到消费的父模块中。加载所有可能的依赖项还会增加负载大小,从而增加页面加载所需的时间。

第二种选择是即时获取依赖项,但可能会在UI的响应中引入任意延迟(参见图3).在这个选项中,只加载需要的模块,以额外的异步请求为代价。如果任何一个搜索模块有额外的依赖项,那么在初始化搜索之前,还会有另一个请求,以此类推。

这两种选择都是不可取的,并且已经被证明对用户体验有显著的负面影响。3.他们也没有考虑到某些个性化方面只在服务器上可用,并且出于安全原因不能向JavaScript层公开的可能性。

回到顶部

大数字改变一切

Netflix网站存储库统计了600多个独特的JavaScript文件和500多个独特的层叠样式表(CSS)文件。A/B测试占了这些文件的绝大多数。我们可以使用独特组合公式来估计网站处理的不同JavaScript有效负载的数量:

ins01.gif

假设总共有600个模块,并估计平均JavaScript有效负载包括大约40个模块,你会得到以下可能的组合数量:

ins02.gif

这个数字很吸引眼球,但并不完全诚实。在600个不同的模块中,大多数都不能独立选择。其中许多模块依赖于其他公共平台模块,而这些模块又依赖于第三方模块。此外,即使是最大规模的A/B测试通常也不会影响到300万用户。这似乎是一个庞大的测试群体,但实际上它仍然只是5000多万用户基础中的一小部分。这些信息导致了一些早期结论:首先,测试的分配不够大,不能均匀地分布到Netflix的所有订阅者;其次,独立选择的文件数量非常少。这两种方法都将显著减少独特组合的数量。

与其试图调整公式,不如分享一些经验数据更为实际。该网站以周为周期部署了一个新的构建。对于每个构建周期,网站产生了大约250万个独特的JavaScript和CSS有效负载组合。

考虑到这个巨大的数字,很容易让浏览器在解析树时获取依赖项。此解决方案适用于小型代码存储库,因为额外的串行请求可能相对不重要。然而,正如前面提到的,由于a /B测试的规模,网站上一个典型的有效载荷包含30到50个不同的模块。即使可以利用浏览器的并行资源获取来获得最大的效率,但在整个过程中累积的延迟潜在的30个以上的请求足以造成次优体验。在图4,即使是深度仅为5个节点的显著简化示例,页面也会在页面就绪之前发出4个异步请求。一个真正的制作页面可能很容易有15个以上的深度。

由于异步加载依赖已经不符合这种特殊情况,很明显,A/B测试的规模决定了交付单个JavaScript负载的选择。如果单个有效载荷是解决方案,这可能会给人一种印象,即这250万个有效载荷是提前生成的。这就需要对每个部署周期中的所有个性化方面进行分析,以便为每个可能的测试组合构建正确的有效负载。然而,如果用户和A/B测试继续沿着正确的轨道增长,那么先发制人的有效负载将变得难以维持。唯一有效载荷的数量今天可能是250万个,明天可能是500万个。对于Netflix来说,这根本不是正确的长期解决方案。

A/B测试系统需要的是一种方法,通过这种方法可以解决条件依赖性,而不会对用户体验产生负面影响。在这种情况下,服务器端组件必须介入,以防止客户端JavaScript因自身的复杂性而崩溃。由于我们能够通过静态分析确定所有可能的依赖关系,以及触发包含每个依赖关系的条件,因此给定我们的需求的最佳解决方案是解决所有条件依赖关系当负载生成时即时。

回到顶部

即时依赖分辨率

让我们向搜索框测试定义添加另一列(参见表2).该表现在表示构建有效负载所需的所有数据的完整抽象。实际上,最后的列映射只存在于UI层,而不存在于提供A/B测试定义的核心服务中。通常,构建这种映射是由测试定义的消费者决定的,因为它对于每个设备或平台来说很可能是唯一的。不过,就本文的目的而言,在单个位置可视化数据更容易一些。

假设有效负载包含中所示主页的文件图5.浏览器要求主页JavaScript有效负载。通过静态分析创建了一个依赖关系树,并有一个表将搜索模块映射到三个可能的实现。由于头文件只关心包含了一个搜索模块,而不关心它的实现,我们可以通过确保所有实现都符合特定的契约(即公共API)来插入正确的搜索模块,如图6

让单一体验的变体符合类似的公共API,允许我们通过简单地包括正确的搜索模块来改变底层实现。不幸的是,由于JavaScript的弱类型特性,没有办法强制执行该契约,甚至没有办法验证任何声称符合该契约的模块的有效性。做正确的事情的责任通常留给创建和使用这些共享模块的开发人员。在实践中,不符合规范的模块并不是游戏的破坏者;上一个示例中的“插入”替换通常是完全自包含的,除了单个入口点(在本例中是公开的入口点)初始化()方法。具有复杂公共api的模块往往是共享的公共库,不太可能以这种方式进行A/B测试。

同样值得注意的是,每一种A/B体验之间的差异通常会决定是否有可能进行临时替换。在某些情况下,新体验是有意设计的,甚至可能是完全不同的,在公共API中有差异是有意义的。这几乎肯定会增加消费父模块的复杂性,但这是同时运行完全不同的体验的可接受成本。其他策略可以帮助降低复杂性,例如返回模块存根(参见图7),而不是尝试真正的替代。在这种情况下,模块加载器可以配置为返回一个带有stub标志的空对象,表明它不是一个真正的实现。如果所讨论的A/B经验几乎没有任何共同之处,并且从公共API中获益甚少(如果有的话),那么这种策略会很有用。

继续主页有效负载的例子,当一个请求请求主页有效负载时(参见图8),我们已经知道订阅者可能收到的所有可能的文件,这是静态分析的结果。

当我们开始向有效负载追加文件时,我们可以在搜索框测试表中查找(表2)是否支持该文件的资格要求(即,订阅者是否符合该特性的资格)。此解析将返回一个布尔值,该值用于确定文件是否被追加(图9).

使用静态分析的组合来构建依赖关系树,然后在请求时使用它来解析条件依赖关系,我们能够为数百万个独特的体验构建定制的有效负载Netflix.com.需要注意的是,这只是最终将JavaScript交付给最终用户的服务链的第一步。

出于性能原因,通过内联脚本交付整个有效负载是不可取的。内联脚本不能独立于HTML内容进行缓存,因此浏览器端缓存的好处马上就失去了。更可取的做法是通过脚本标记交付它,该标记指向表示该有效负载的URL,浏览器可以轻松缓存该有效负载。在大多数情况下,这是一个CDN(内容分发网络)托管的URL,其原始服务器指向生成此有效负载的原始服务器。因此,到目前为止讨论的所有内容都只负责生成有效载荷的惟一性。

然而,仅仅缓存具有随机生成的标识符的唯一有效负载是不够的。如果服务器有多个实例运行以实现负载平衡,那么这些实例中的任何一个都可以接收此负载的传入请求。如果请求转到尚未生成(或缓存)唯一有效负载的实例,则无法解析请求。要解决这个问题,至关重要的是有效负载的URL是反向可解析的;服务器的任何实例都必须能够通过简单地查看URL来解析唯一有效负载中的文件。这可以通过几种方法来解决,最常见的方法是通过在URL中直接引用文件名来表示文件,或者使用独特的散列组合,其中每个散列块都可以解析为特定的文件。

回到顶部

未来的优化

虽然我们已经针对单个有效负载进行了优化,但仍然有可能使用并行浏览器请求来获得额外的性能提升。我们希望避免解绑定整个有效负载,这将迫使我们采取30多个请求的方式,但我们可以将单个有效负载分成两个,第一个包含所有常见的第三方库或共享模块,第二个包包含特定于页面的模块。这将允许浏览器在页面与页面之间缓存通用模块,进一步减少用户在站点中移动时页面就绪的时间上限。这在Web浏览器通常必须处理的带宽和延迟限制之间取得了很好的平衡。

回到顶部

期待

尽管有这些缺点,JavaScript已经成为事实上的Web语言,并且随着行业的发展,它将继续在无数的设备和平台上使用。本文中发现的问题只是冰山一角,尤其是随着应用程序的规模和复杂性的增长。事实上,JavaScript主要还是一个客户端-side语言,其运行时环境主要是浏览器。这意味着大多数用于解决复杂问题(如条件依赖)的库或工具已经接近并试图从浏览器领域解决这个问题。

从浏览器内解决问题的方法的局限性限制了更丰富的端到端解决方案的可能性。虽然即将到来的ECMAScript 6版本的更新同时提供了本地JavaScript模块和模块加载器,但它也同样存在范围受限的问题。即使是现在最完整的模块系统也只能从浏览器领域内解决这个问题。

正如我们所发现的,通配符约束是浏览器运行时环境距离源代码(服务器)的位置“太远”。以往,较大的web开发团队都不愿意开发将服务器和浏览器域紧密集成的解决方案。这样做的原因很可能是简单,或者是希望更清楚地分离客户端和服务器端代码之间的关注点。然而,条件依赖项使这个约束的存在变得非常清楚。任何未能考虑到这一点的解决方案都将不可避免地留下一些性能问题。因此,解决条件依赖关系的性能最好的JavaScript打包解决方案将需要一个服务器端组件,至少在可预见的未来是这样。

随着Node.js和JavaScript在服务器上的崛起,我们今天面临的问题完全有可能会得到更多的曝光。在许多企业环境中,服务器是一个完全独立的领域,由具有不同技能的工程师拥有。然而,Node.js为许多前端工程师向服务器端转移打开了大门,不仅扩展了传统前端工程师的角色,还扩展了解决ui特定问题的工具集。这种模式的转变,以及前端工程师的新角色扩展,确实给未来带来了一些希望。由于前端工程师拥有UI服务器,他们可以对所有特定于UI的问题进行端到端控制。

本文中讨论的解决方案正是在这种环境下诞生的,这是未来的一个好迹象。尽管JavaScript缺乏处理当今web应用程序密集世界中的一些问题的约定,但一些创造性的工程可以在此期间填补这一空白。解决复杂的问题,如条件依赖,使Netflix有可能继续构建大型、复杂和迷人的用户界面-所有这些都是JavaScript。该网站最近采用的Node.js是对该语言和Netflix对开放网络承诺的最好的认可。

ACM队列的q戳相关文章
queue.acm.org

陶醉于约束
布鲁斯·约翰逊
http://queue.acm.org/detail.cfm?id=1572457

跳中的多层编程
Manuel Serrano和Gérard Berry
http://queue.acm.org/detail.cfm?id=2330089

Antifragile组织
阿里尔Tseitlin
http://queue.acm.org/detail.cfm?id=2499552

回到顶部

参考文献

1.延迟:新的Web性能瓶颈;https://www.igvita.com/2012/07/19/latency-the-new-web-performance-bottleneck/

2.HTTP存档。趋势;http://httparchive.org/trends.php?s=All&minlabel=Nov+15+2010&maxlabel=Jun+15+2014

3.响应时间:三个重要的限制(1993年更新2014年);http://www.nngroup.com/articles/response-times-3-important-limits/

4.JavaScript:在10天内设计一门语言。电脑45, 2 (2012), 7-8;http://www.computer.org/csdl/mags/co/2012/02/mco2012020007.html

回到顶部

作者

亚历克斯·刘是Netflix的高级UI工程师,也是核心团队的一员,领导着Netflix.comnode . js。他的整个职业生涯都在桌面、浏览器和服务器上构建JavaScript应用程序,但他仍然对社区利用JavaScript的所有新颖和创造性的方式感到惊讶。

回到顶部

数据

F1图1。AMD模块因RequireJS等库而普及。

F2图2。一个头模块,加载所有三个搜索特性,但只使用其中一个。

F3图3。一个头模块,只加载所需的模块。

F4图4。让浏览器获取依赖项。

F5图5。主页上的负载。

F6图6。三种搜索体验使用相同的公共API返回不同的实现。

F7图7。返回模块存根的策略。

F8图8。示例主页有效负载的扩展依赖树。

F9图9。在请求时删除不符合条件的文件的依赖树。

回到顶部

T1表1。搜索框测试。

T2表2。带有到模块的附加映射的搜索框测试。

回到顶部


版权归所有者/作者所有。授权给ACM的出版物权利。

数字图书馆是由计算机协会出版的。版权所有©2014 ACM股份有限公司


没有发现记录

登录为完全访问
»忘记密码? *创建ACM Web帐户
文章内容:
Baidu
map