Поддержка технологии WebAssembly (Wasm) появилась в браузерах относительно недавно. Но эта технология вполне может серьёзно расширить возможности веба, сделав его платформой, способной поддерживать такие приложения, которые обычно воспринимаются как настольные.
Освоение WebAssembly может оказаться непростым делом для веб-разработчиков. Однако ситуацию способен улучшить компилятор AssemblyScript.
Автор статьи, перевод которой мы сегодня публикуем, предлагает сначала поговорить о том, почему WebAssembly — это весьма многообещающая технология, а потом — взглянуть на то, как AssemblyScript может помочь в раскрытии потенциала Wasm.
WebAssembly
WebAssembly можно назвать низкоуровневым языком для браузеров. Он даёт разработчикам возможность создавать код, компилирующийся в нечто, отличающееся от JavaScript. Это позволяет программам, входящим в состав веб-страниц, работать почти так же быстро, как нативные приложения для различных платформ. Такие программы выполняются в ограниченном безопасном окружении.
К созданию стандарта WebAssembly причастны представители команд, ответственных за разработку всех ведущих браузеров (Chrome, Firefox, Safari и Edge). Они достигли согласия относительно архитектуры системы в начале 2017 года. Сейчас все вышеупомянутые браузеры поддерживают WebAssembly. Фактически, пользоваться этой технологией можно примерно в 87% браузеров.
WebAssembly-код существует в бинарном формате. Это означает, что такой код меньше, чем аналогичный JavaScript-код, и быстрее загружается. Wasm-код, кроме того, может быть представлен в текстовом формате, благодаря чему его могут читать и редактировать люди.
Когда стандарт WebAssembly только появился, некоторые разработчики думали, что он вполне может занять место JavaScript и стать основным языком веба. Но WebAssembly лучше воспринимать как новый инструмент, который хорошо интегрируется в существующую веб-платформу. В этом заключается одна из его приоритетных целей.
Вместо того, чтобы заменять JavaScript там, где этот язык уже давно и успешно используется, WebAssembly даёт веб-разработчикам новые интересные возможности. Правда, у Wasm-кода нет прямого доступа к DOM, поэтому большинство существующих веб-проектов будут продолжать использовать JavaScript. Этот язык, за годы развития и оптимизации, уже достаточно быстр. А у WebAssembly есть собственные сферы применения:
- Игры.
- Научные расчёты, визуализации, симуляции.
- CAD-приложения.
- Редактирование изображений и видео.
Все эти варианты использования Wasm объединяет то, что соответствующие приложения обычно рассматривают как настольные. Но благодаря тому, что WebAssembly позволяет выйти на уровень производительности, близкий к нативному, множество подобных приложений можно реализовать теперь и средствами веб-платформы.
Возможностями WebAssembly могут воспользоваться и существующие веб-проекты. В качестве примера можно привести проект Figma. Благодаря применению Wasm удалось значительно улучшить время загрузки этого проекта. Если веб-сайт использует код, выполняющий тяжёлые вычисления, то, ради повышения производительности этого сайта, имеет смысл заменить WebAssembly-аналогом только такой код.
Возможно, вы хотите попробовать воспользоваться WebAssembly в собственных проектах. Этот язык вполне можно изучить и писать сразу на нём. Но, всё же, WebAssembly изначально разрабатывался как цель компиляции для других языков. Он был спроектирован с учётом хорошей поддержки C и C++. Экспериментальная поддержка Wasm появилась в Go 1.11. Немало сил вкладывается и в то, чтобы Wasm-приложения можно было бы писать на Rust.
Но вполне возможно то, что разработчикам, владеющим веб-технологиями, не захочется изучать C, C++, Go или Rust только для того, чтобы пользоваться WebAssembly. Что же им делать? Ответ на этот вопрос может дать AssemblyScript.
AssemblyScript
AssemblyScript — это компилятор, преобразующий TypeScript-код в WebAssembly-код. TypeScript — это язык, разработанный Microsoft. Это — надмножество JavaScript, отличающееся улучшенной поддержкой типов и некоторыми другими возможностями. TypeScript стал довольно популярным языком. При этом надо отметить, что AssemblyScript способен преобразовать в Wasm лишь ограниченный набор конструкций TypeScript. Это значит, что даже тот, кто не знаком с TypeScript, сможет достаточно быстро освоить этот язык на уровне, достаточном для написания кода, понятного AssemblyScript.
При этом, учитывая то, что TypeScript очень похож на JavaScript, можно сказать, что технология AssemblyScript позволяет веб-разработчикам без особых сложностей интегрировать в свои проекты Wasm-модули и при этом не сталкиваться с необходимостью изучения совершенно нового языка.
Пример
Давайте напишем наш первый AssemblyScript-модуль. Весь код, который мы будем сейчас обсуждать, можно найти на GitHub. Для обеспечения поддержки WebAssembly нам понадобится, как минимум, Node.js 8.
Создадим новую директорию, инициализируем npm-проект и установим AssemblyScript:
mkdir assemblyscript-demo
cd assemblyscript-demo
npm init
npm install --save-dev github:AssemblyScript/assemblyscript
Обратите внимание на то, что AssemblyScript нужно установить непосредственно из GitHub-репозитория проекта. AssemblyScript пока не опубликован в npm, так как разработчики ещё не считают его готовым к широкому применению.
Создадим вспомогательные файлы с помощью включённой в состав AssemblyScript команды asinit
:
npx asinit .
Теперь раздел scripts
нашего package.json
должен принять следующий вид:
{
"scripts": {
"asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
"asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
"asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"
}
}
Файл index.js
, находящийся в корневой папке проекта, будет выглядеть так:
const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {
env: {
abort(_msg, _file, line, column) {
console.error("abort called at index.ts:" + line + ":" + column);
}
}
};
Object.defineProperty(module, "exports", {
get: () => new WebAssembly.Instance(compiled, imports).exports
});
Это позволяет подключать в коде WebAssembly-модули с помощью команды require. То есть — так же, как подключаются и обычные JavaScript-модули.
Папка assembly
содержит файл index.ts
. В нём имеется исходный код, написанный по правилам AssemblyScript. Автоматически созданный шаблонный код представляет собой простую функцию для сложения двух чисел:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Возможно, вы ожидали, что сигнатура подобной функции будет выглядеть как add(a: number, b: number): number
. Так она выглядела бы, будь она написана на обычном TypeScript. Но здесь вместо типа number
используется тип i32
. Происходит это из-за того, что в AssemblyScript-коде применяются специфические типы WebAssembly для целых чисел и чисел с плавающей точкой, а не универсальный тип number из TypeScript.
Соберём проект:
npm run asbuild
В папке build
должны появиться следующие файлы:
optimized.wasm
optimized.wasm.map
optimized.wat
untouched.wasm
untouched.wasm.map
untouched.wat
Здесь имеются оптимизированная и обычная версии сборки. Каждая версия сборки даёт в наше распоряжение бинарный .wasm-файл, карту кода .wasm.map и текстовое представление бинарного кода в .wat-файле. Тестовое представление Wasm-кода предназначено для программиста, но мы не будем даже заглядывать в этот файл. Собственно говоря, одна из причин использования AssemblyScript заключается в том, что это избавляет от необходимости работать с Wasm-кодом.
Теперь давайте запустим Node.js в режиме REPL и убедимся в том, что скомпилированным Wasm-модулем можно пользоваться точно так же, как и любым обычным JS-модулем:
$ node
Welcome to Node.js v12.10.0.
Type ".help" for more information.
> const add = require('./index').add;
undefined
> add(3, 5)
8
В общем-то — это всё, что нужно для того, чтобы пользоваться технологией WebAssembly в среде Node.js.
Оснащение проекта скриптом-наблюдателем
Для того чтобы в ходе разработки автоматически пересобирать модуль при внесении в него изменений, я рекомендую пользоваться пакетом onchange. Дело в том, что в AssemblyScript пока нет собственной системы наблюдения за изменениями файлов. Установим пакет onchange:
npm install --save-dev onchange
Добавим в package.json
скрипт asbuild:watch
. Флаг -i
включён в команду для того, чтобы процесс сборки один раз запускался бы при вызове скрипта, до возникновения каких-либо событий.
{
"scripts": {
"asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
"asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
"asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
"asbuild:watch": "onchange -i 'assembly/**/*' -- npm run asbuild"
}
}
Теперь, вместо того, чтобы постоянно запускать команду asbuild
, достаточно один раз запустить asbuild:watch
.
Производительность
Напишем простой тест, который позволит оценить уровень производительности Wasm-кода. Основная сфера применения WebAssembly — это решение задач, интенсивно использующих процессор. Например — это какие-нибудь «тяжёлые» вычисления. Создадим функцию, которая выясняет, является ли некое число простым.
Ниже показана базовая JS-реализация подобной функции. Устроена она очень просто, проверяет число методом перебора, но для наших целей она подходит, так как выполняет большие объёмы вычислений.
function isPrime(x) {
if (x < 2) {
return false;
}
for (let i = 2; i < x; i++) {
if (x % i === 0) {
return false;
}
}
return true;
}
Аналогичная функция, написанная в расчёте на компилятор AssemblyScript, выглядит почти так же. Главное отличие — присутствие в коде аннотаций типов:
function isPrime(x: u32): bool {
if (x < 2) {
return false;
}
for (let i: u32 = 2; i < x; i++) {
if (x % i === 0) {
return false;
}
}
return true;
}
Для анализа производительности кода будем пользоваться пакетом Benchmark.js. Установим его:
npm install --save-dev benchmark
Создадим файл benchmark.js
:
const Benchmark = require('benchmark');
const assemblyScriptIsPrime = require('./index').isPrime;
function isPrime(x) {
for (let i = 2; i < x; i++) {
if (x % i === 0) {
return false;
}
}
return true;
}
const suite = new Benchmark.Suite;
const startNumber = 2;
const stopNumber = 10000;
suite.add('AssemblyScript isPrime', function () {
for (let i = startNumber; i < stopNumber; i++) {
assemblyScriptIsPrime(i);
}
}).add('JavaScript isPrime', function () {
for (let i = startNumber; i < stopNumber; i++) {
isPrime(i);
}
}).on('cycle', function (event) {
console.log(String(event.target));
}).on('complete', function () {
const fastest = this.filter('fastest');
const slowest = this.filter('slowest');
const difference = (fastest.map('hz') - slowest.map('hz')) / slowest.map('hz') * 100;
console.log(`${fastest.map('name')} is ~${difference.toFixed(1)}% faster.`);
}).run();
Вот что мне удалось получить после выполнения команды node benchmark
на моём компьютере:
AssemblyScript isPrime x 74.00 ops/sec ±0.43% (76 runs sampled)
JavaScript isPrime x 61.56 ops/sec ±0.30% (64 runs sampled)
AssemblyScript isPrime is ~20.2% faster.
Как видно, AssemblyScript-реализация алгоритма оказалась на 20% быстрее JS-реализации. Однако обратите внимание на то, что этот тест представляет собой микробенчмарк. Не стоит слишком сильно полагаться на его результаты.
Для того чтобы найти более надёжные результаты исследования производительности AssemblyScript-проектов — рекомендую взглянуть на этот и этот бенчмарки.
Использование Wasm-модуля на веб-странице
Давайте воспользуемся нашим Wasm-модулем на веб-странице. Начнём с создания файла index.html
со следующим содержимым:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AssemblyScript isPrime demo</title>
</head>
<body>
<form id="prime-checker">
<label for="number">Enter a number to check if it is prime:</label>
<input name="number" type="number" />
<button type="submit">Submit</button>
</form>
<p id="result"></p>
<script src="demo.js"></script>
</body>
</html>
Теперь создадим файл demo.js
, код которого показан ниже. Существует множество способов загрузки WebAssembly-модулей. Самый эффективный — это их компиляция и инициализация в потоковом режиме с помощью функции WebAssembly.instantiateStreaming. Обратите внимание на то, что нам тут понадобится переопределить функцию abort, вызываемую в том случае, если не выполняется некое утверждение.
(async () => {
const importObject = {
env: {
abort(_msg, _file, line, column) {
console.error("abort called at index.ts:" + line + ":" + column);
}
}
};
const module = await WebAssembly.instantiateStreaming(
fetch("build/optimized.wasm"),
importObject
);
const isPrime = module.instance.exports.isPrime;
const result = document.querySelector("#result");
document.querySelector("#prime-checker").addEventListener("submit", event => {
event.preventDefault();
result.innerText = "";
const number = event.target.elements.number.value;
result.innerText = `${number} is ${isPrime(number) ? '' : 'not '}prime.`;
});
})();
Далее, установим пакет static-server. Сервер нам нужен из-за того, что для использования функции WebAssembly.instantiateStreaming
модуль надо обслуживать с использованием MIME-типа application/wasm
.
npm install --save-dev static-server
Добавим в package.json
соответствующий скрипт:
{
"scripts": {
"serve-demo": "static-server"
}
}
Теперь выполним команду npm run serve-demo
и откроем в браузере URL локального хоста. Если ввести в форму некое число — можно узнать о том, простое оно или нет. Теперь мы, в деле освоения AssemblyScript, прошли полный путь — от написания кода до использования его в среде Node.js и на веб-странице.
Итоги
WebAssembly и, следовательно, AssemblyScript, неспособны неким волшебным образом ускорить любой сайт. Но перед Wasm никогда и не ставилась задача ускорения абсолютно всего. Эта технология замечательна тем, что открывает дорогу в веб новым видам приложений.
Нечто похожее можно сказать и об AssemblyScript. Эта технология упрощает доступ к WebAssembly большому количеству разработчиков. Она позволяет, создавая код на языке, близком к JavaScript, пользоваться возможностями WebAssembly для решения сложных вычислительных задач.
Уважаемые читатели! Как вы оцениваете перспективы использования AssemblyScript в своих проектах?
Автор: ru_vds