Поддержка технологии 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 в своих проектах?


Комментарии (6)


  1. JustDont
    27.11.2019 12:50

    Дочитал до

    WebAssembly-код существует в бинарном формате. Это означает, что такой код меньше, чем аналогичный JavaScript-код, и быстрее загружается.

    Сильно поржал. Отличный уровень статьи. Автору, видимо, было неведомо, что чтоб написать такой wasm, который будет короче некоего JS — нужно написать очень очень много wasm и JS. В противном случае оверхед сожрёт всё напрочь, в то время как для JS оверхед отсутствует (всё уже есть в браузере).

    PS: Чтоб не быть голословным — пример из статьи, имеющий 1 функцию, складывающую два числа — представляет собой бинарный файл на 4 килобайта. Что-то там говорите, короче и меньше чего?


    1. harry2019
      27.11.2019 17:00

      Стало интересно, подумал, что ошибка перевода, залез в оригинал статьи.

      «WebAssembly is delivered in a binary format, which means that it has both size and load time advantages over JavaScript.» я бы по другому и не перевел.

      Правда, непонятно, что имелось в виду. Как он может быть короче? Время загрузки — спорить не буду (возможно), но размер…


      1. JustDont
        27.11.2019 17:15

        Чистый байт-код wasm конечно же короче такого же кода на JS (который строка). Разница именно в том, что в модуль wasm входит не только чистый код, который у вас в исходниках, а еще много другого интересного. На достаточно больших объемах wasm без проблем обгонит JS, вот только до таких объемов кода еще дожить надо; огромному количеству сайтов (даже на модных фреймворках, если их применяли адекватным образом) JS надо довольно-таки мало. А уж если вспомнить про сжатие в HTTP, то всё становится еще грустнее для wasm.

        В настоящее время wasm позволяет сделать неплохую числодробилку в браузере, для тех случаев, когда она реально нужна. Но на этом всё. И, опять же, быстродействие wasm — штука очень тонкая, с учётом того, что JS в браузере тоже здорово разгоняется засчёт оптимизации выполнения, пусть и не сразу; а у быстрого wasm есть бутылочное горлышко на отправке/принятии данных.


  1. CoreTeamTech
    27.11.2019 13:23

    Автор оригинала также забыл упомянуть, что используется не весь TypeScript, а его подмножество или, я бы сказал, вариация. Там довольно много ограничений, и есть ложное впечатление, что это тот самый TS, который вы используете для фронтенда. Я нахожу противоречивым использовать динамические языки или их вариации для WASM.


  1. alekslitvinenk
    27.11.2019 21:29
    +1

    Для меня аббревиатура AS прочно ассоциируется с ActionScript (Flash)


  1. radist2s
    28.11.2019 15:32

    Когда узнал о проекте впервые — его будущее было довольно неопределенным, а популярность даже по тем же звёздам на гитхабе сомнительная. Год спустя я читаю об AssemblyScript на Хабре. Я все ещё офигеваю как робкая инициатива одного человека может повлиять на целую индустрию, если ты делаешь что-то по-кайфу именно для себя.


    AssemblyScript, конечно же далек от TypeScript. Точно так же, как JS далек от TS.
    Однако, разработчик на JS без труда может поправить код на TS(всякое бывает), запустить тесты и со спокойной душой сделать деплой. Точно так же, при необходимости, можно и легко исправить код на AS, запустить тесты и деплойнуть. Напомню, что все это потребует от разработка только установки модулей проекта, без необходимости ставить другое ПО и погружаться совершенно в другой контекст. А в случае AS у вас ещё и синтаксис наследуется от TS, а это далеко не то же самое, чем фигачить сразу на C/C++.