WebAssembly:解决Web上的性能问题
在现代JavaScript中,目标通常是找到所有优化浏览器性能的方法。有些时候,web应用程序需要高性能,并期望浏览器能够跟上。
由于引擎处理语言的方式,传统JavaScript有性能限制。作为页面的一部分呈现的解释性(甚至是jit编译)语言只能得到这么多——即使从最强大的硬件上也是如此。
WebAssembly完全是为了解决性能问题而设计的。它可以克服传统JavaScript无法解决的瓶颈问题。在WebAssembly中,不需要解析和解释代码。WebAssembly充分利用其字节码格式,为您提供与本机程序相匹配的运行时速度。
从另一种角度考虑:将传统JavaScript想象成一种好的、万能的工具,可以让您到达任何地方。相反,WebAssembly是一种高性能的解决方案,能够达到接近本机的速度。这是两个独立的编程工具。
我的问题是:WebAssembly会取代传统的JavaScript吗?如果不是,是否值得投资学习WebAssembly?
什么是WebAssembly?
WebAssembly是一种可以发送到浏览器的不同类型的代码。它在字节码,这意味着它在到达浏览器之前已经以低级汇编语言发布。字节码不是手工编写的,但可以从任何编程语言(如c++或Rust)编译。然后浏览器可以获取任何WebAssembly代码,将其作为本机代码加载,从而实现高性能。
您可以将此WebAssembly字节码视为一个模块:浏览器可以获取、加载并执行该模块。每个WebAssembly模块都具有导入和导出功能,其行为与JavaScript对象非常相似。WebAssembly模块的工作原理与任何其他JavaScript代码非常相似,只是它以接近本机的速度运行。从程序员的角度来看,您可以使用与当前JavaScript对象相同的方式来使用WebAssembly模块。这意味着你对JavaScript和web的了解也可以转移到WebAssembly编程中。
WebAssembly工具通常由c++编译器组成。在当前的开发中有许多工具,但是已经成熟的工具是Emscripten.该工具将c++代码编译成WebAssembly模块,并构建可以在任何地方运行的符合标准的模块。编译后的输出将有一个WASM文件扩展名,以表明它是一个WebAssembly模块。
WebAssembly的一个优点是,当你获取模块时,你有所有相同的HTTP缓存头。另外,您可以使用IndexedDB缓存WASM模块,也可以使用会话存储.缓存策略围绕着缓存获取API请求,并通过保留本地副本来避免另一个请求。因为WebAssembly模块是字节码格式的,所以你可以把这个模块当作一个字节数组并存储在本地。
既然我们知道了WebAssembly是什么,那么它的局限性是什么呢?
已知的限制
JavaScript运行在不同于任何典型c++程序的环境中。因此,限制包括本机api在浏览器环境中可以做什么。
网络函数必须是异步和非阻塞操作。所有底层JavaScript网络功能在浏览器的Web API中都是异步的。然而,WebAssembly并不能从异步I/ o绑定操作中获益。I/O操作必须等待网络响应,这使得所有接近本机的性能增益可以忽略不计。
在浏览器中运行的代码,在沙箱环境中运行,并且不能访问文件系统。您可以创建内存中的虚拟文件系统,而不是预先加载数据。
应用程序的主循环使用合作多任务处理,每个事件都轮流执行。web上的事件通常来自鼠标点击、手指点击或拖放操作。事件必须将控制权返回给浏览器,以便处理其他事件。避免劫持主事件循环是明智的,因为这可能变成调试的噩梦。DOM事件通常绑定到UI更新,这是昂贵的。这给我们带来了另一个限制。
WebAssembly不能访问DOM;它依赖于JavaScript函数来进行任何更改。目前,有一项提案允许与web上的DOM对象的互操作性.仔细想想,DOM重绘既缓慢又昂贵。从近乎本地的性能中获得的所有好处都被DOM破坏了。一种解决方案是将DOM抽象为内存中的本地副本,稍后可以通过JavaScript进行协调。
在WebAssembly中,一些好的建议是坚持执行速度非常快的东西。在工作中使用能产生最大性能收益的工具,同时避免陷阱。可以把WebAssembly看作是一个超高速的系统,它可以在没有任何阻塞的情况下独立运行。
WebAssembly中的浏览器兼容性很差,除了现代浏览器。IE中没有支持。然而,Edge 16+支持WebAssembly。所有现代的大型播放器,如Firefox 52+, Safari 11+和Chrome 57+都支持WebAssembly。一个想法是在WebAssembly模块和JavaScript之间进行特性检测和特性对等。这样你就不会破坏网页,现代浏览器从WebAssembly中获得所有的性能提升。
WebAssembly演示
足够的讨论;是时候做一组不错的演示了。这次我们将探索WebAssembly中的导出和导入功能。导出和导入功能是WebAssembly互操作性的标志。这些函数使程序员能够像使用其他JavaScript对象一样使用WebAssembly模块。
一个导出功能是你从WebAssembly模块中获得的。一旦模块加载,您将在其中找到导出函数instance.exports
.对于这个演示,我将导出一个添加
函数,它计算作为参数传入的两个数字的和。计算将在接近本地的WebAssembly代码中执行。在这个演示中,导出函数将是一个纯JavaScript函数——这意味着它是无状态且不可变的。
一个导入功能是你提供给WebAssembly模块的一个。它是一个简单的JavaScript对象,有一个回调函数。然后,模块用WebAssembly中的参数调用函数。我将导入一个简单的回调,它从WebAssembly接收一个参数。参数是一个赋值为42的常量。然后我将使用这个值从JavaScript中设置DOM:
<跨度>p跨度><跨度class="token punctuation">>跨度>跨度>添加结果:<跨度class="token tag"><跨度>跨度跨度><跨度class="token attr-name">id跨度><跨度class="token attr-value">=跨度><跨度class="token punctuation">"跨度>addResult<跨度class="token punctuation">"跨度>跨度><跨度class="token punctuation">>跨度>跨度><跨度class="token tag">跨度>跨度跨度><跨度class="token punctuation">>跨度>跨度><跨度class="token tag">跨度>p跨度><跨度class="token punctuation">>跨度>跨度><跨度class="token tag"><跨度>p跨度><跨度class="token punctuation">>跨度>跨度>简单的结果:<跨度class="token tag"><跨度>跨度跨度><跨度class="token attr-name">id跨度><跨度class="token attr-value">=跨度><跨度class="token punctuation">"跨度>simpleResult<跨度class="token punctuation">"跨度>跨度><跨度class="token punctuation">>跨度>跨度><跨度class="token tag">跨度>跨度跨度><跨度class="token punctuation">>跨度>跨度><跨度class="token tag">跨度>p跨度><跨度class="token punctuation">>跨度>跨度>
导出WebAssembly函数
首先,让我们看一下WebAssembly模块的文本格式。这是WASM模块的文本表示形式,可以由人类读取。它是为文本编辑器或任何其他可以使用纯文本的工具设计的:
(module (func $add (param $lhs i32) (param $rhs i32) (result i32) get_local $lhs get_local $rhs i32) (export "add" (func $add)))
理解这里的每个细节并不太重要。这是WebAssembly模块的文本格式,您经常可以在WAT文件扩展名中找到它。的i32.add
使用近本地代码执行添加。的出口“添加”
然后抓住函数添加美元
并使它对JavaScript可用。
要加载WebAssembly模块,你可以这样做:
WASM模块的URL跨度><跨度class="token keyword">常量跨度><跨度class="token constant">WASM_ADD_MODULE跨度><跨度class="token operator">=跨度><跨度class="token string">“https://myhost.com/add.wasm”跨度><跨度class="token punctuation">;跨度><跨度class="token function">获取跨度><跨度class="token punctuation">(跨度><跨度class="token constant">WASM_ADD_MODULE跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">然后跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">响应跨度><跨度class="token arrow operator">= >跨度>响应<跨度class="token punctuation">.跨度><跨度class="token method function property-access">arrayBuffer跨度><跨度class="token punctuation">(跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">然后跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">字节跨度><跨度class="token arrow operator">= >跨度><跨度class="token known-class-name class-name">WebAssembly跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">实例化跨度><跨度class="token punctuation">(跨度>字节<跨度class="token punctuation">)跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">然后跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">结果跨度><跨度class="token arrow operator">= >跨度><跨度class="token dom variable">文档跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">getElementById跨度><跨度class="token punctuation">(跨度><跨度class="token string">“addResult”跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token property-access">innerHTML跨度><跨度class="token operator">=跨度>结果<跨度class="token punctuation">.跨度><跨度class="token property-access">实例跨度><跨度class="token punctuation">.跨度><跨度class="token property-access">出口跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">添加跨度><跨度class="token punctuation">(跨度><跨度class="token number">1跨度><跨度class="token punctuation">,跨度><跨度class="token number">5跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">;跨度>
Fetch API从URL获取模块,并将其转换为字节数组。这个字节数组来自response.arrayBuffer
.注意,检查导出的函数exports.add
表示它被编译为本机代码:
函数跨度><跨度class="token number">0跨度><跨度class="token punctuation">(跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">{跨度><跨度class="token punctuation">[跨度>本机代码<跨度class="token punctuation">]跨度><跨度class="token punctuation">}跨度>
一个问题是使用WebAssembly.instantiate
是比宽大吗WebAssembly.instantiateStreaming
.后者表示WASM模块必须具有MIME类型应用程序/ wasm
.你会遇到这个问题,当你得到TypeError
和它一起工作的时候。如果您通过CDN提供WASM模块,并且不能控制MIME类型,那么使用WebAssembly.instantiate
.WebAssembly.instantiateStreaming
比前者更高效,但它是一种较新的web API,所以还不能在所有现代浏览器中使用。
导入WebAssembly函数
对于导入的函数,请以文本格式从此模块开始。想象一下在WebAssembly中进行这个cpu受限且代价高昂的计算。如此强烈,事实上,这是生命和一切终极问题的答案。
例如:
(module (func $i (import "imports" " importted_func ") (param i32)) (func (export "exported_func") i32.)Const 42调用$i))
注意这个常数手机等。const 42
被宣布。然后,使用导入的函数调用回调函数打电话给我美元
.的出口“exported_func”
声明从JavaScript调用的导出函数的名称。
在JavaScript中,我们可以这样处理这个模块:
常量跨度><跨度class="token constant">WASM_SIMPLE_MODULE跨度><跨度class="token operator">=跨度><跨度class="token string">“https://myhost.com/simple.wasm”跨度><跨度class="token punctuation">;跨度><跨度class="token keyword">常量跨度><跨度class="token function-variable function">simpleFn跨度><跨度class="token operator">=跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">参数跨度><跨度class="token punctuation">)跨度><跨度class="token arrow operator">= >跨度><跨度class="token dom variable">文档跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">getElementById跨度><跨度class="token punctuation">(跨度><跨度class="token string">“simpleResult”跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token property-access">innerHTML跨度><跨度class="token operator">=跨度>参数<跨度class="token punctuation">;跨度><跨度class="token keyword">常量跨度>importSimpleObj<跨度class="token operator">=跨度><跨度class="token punctuation">{跨度>进口<跨度class="token operator">:跨度><跨度class="token punctuation">{跨度>imported_func<跨度class="token operator">:跨度>simpleFn<跨度class="token punctuation">}跨度><跨度class="token punctuation">}跨度><跨度class="token punctuation">;跨度><跨度class="token function">获取跨度><跨度class="token punctuation">(跨度><跨度class="token constant">WASM_SIMPLE_MODULE跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">然后跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">响应跨度><跨度class="token arrow operator">= >跨度>响应<跨度class="token punctuation">.跨度><跨度class="token method function property-access">arrayBuffer跨度><跨度class="token punctuation">(跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">然后跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">字节跨度><跨度class="token arrow operator">= >跨度><跨度class="token known-class-name class-name">WebAssembly跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">实例化跨度><跨度class="token punctuation">(跨度>字节<跨度class="token punctuation">,跨度>importSimpleObj<跨度class="token punctuation">)跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">然后跨度><跨度class="token punctuation">(跨度><跨度class="token parameter">结果跨度><跨度class="token arrow operator">= >跨度>结果<跨度class="token punctuation">.跨度><跨度class="token property-access">实例跨度><跨度class="token punctuation">.跨度><跨度class="token property-access">出口跨度><跨度class="token punctuation">.跨度><跨度class="token method function property-access">exported_func跨度><跨度class="token punctuation">(跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">)跨度><跨度class="token punctuation">;跨度>
看看importSimpleObj
,因为这是具有回调函数的JavaScript对象。的exports.exported_func
然后执行WebAssembly模块。一旦调用,导入的函数simpleFn
使用常量参数运行。
下面是一个你可以玩的CodePen演示。请随意检查此代码示例中的每个函数和对象。这将使您对与WebAssembly集成所需的粘合代码有一个良好的感觉。
结论
回答我最初的问题,WebAssembly是对Web的一个很好的补充。它并不是JavaScript的替代品,只是增强了当前的web技术。任何追求速度、效率和高性能的web工程师都应该关注WebAssembly。JavaScript是执行和处理WebAssembly结果的胶水代码。
一种想法是移植现有的JavaScript代码,这些代码执行大量cpu绑定的工作——比如,DOM的内存虚拟表示,它只抽象真正的DOM。例如,WebAssembly端口也可以为那些还不支持WebAssembly的浏览器提供一个优雅的退步。
随着WebAssembly模块变得越来越普遍,npm包可能会附带这些模块,这些模块背后是漂亮的JavaScript抽象。这既增强了当前的生态系统,又增加了代码重用。也许有一天,你不再需要编写自己的WebAssembly模块。
WebAssembly的可能性是无限的。这是一个您现在就可以添加到您的武器库中的工具—用于解决在Web上遇到的许多性能瓶颈。