Как стать автором
Обновить

Angular на стероидах: наращиваем производительность при помощи WebAssembly

Время на прочтение9 мин
Количество просмотров4.2K
Автор оригинала: Connie Leung
В этом посте продемонстрировано, как с лёгкостью использовать WebAssembly внутри приложения, написанного на Angular. Иногда в приложении на Angular требуется выполнить задачу, которая в JavaScript завершается не слишком быстро. Конечно, можно переписать алгоритм на другом языке, например, AssemblyScript и Rust — и код станет эффективнее. Затем можно скомпилировать получившиеся фрагменты кода в файле WASM и потоком передать двоичные данные в приложение, чтобы можно было вызывать из него функции WASM. Бывает и так, что разработчику не удаётся найти в реестре NPM опенсорсные библиотеки, нужные для решения задачи. В таком случае можно написать пакет не на JS, а на каком-нибудь другом языке, затем скомпилировать этот пакет в WASM и опубликовать код WASM в реестре NPM. Angular-разработчики устанавливают новый пакет как зависимость и выполняют WASM-функции внутри приложения.

В следующем демонстрационном примере я напишу на AssemblyScript несколько функций для работы с простыми числами, а затем опубликую файл индекса в формате WASM. Затем скопирую файл WASM в приложение Angular, потоком отправлю двоичные данные через WebAssembly API и, наконец, стану вызывать эти функции, чтобы с их помощью выполнять различные действия над простыми числами.

Что такое WebAssembly?


WebAssembly – это составное слово, которое делится на 2 части: Web (Веб) и Assembly (Ассемблер). Когда пишешь на высокоуровневых языках, например, на AssemblyScript и Rust, получается код, который компилируется в ассемблер при помощи специальных инструментов. После этого разработчик может нативно выполнять ассемблерный код прямо в браузере, то есть, в вебе.

Как этот демо-пример может использоваться на практике


С этим демо-примером связаны 2 github-репозитория. В первом используется язык AssemblyScript, на котором пишется TypeScript-подобный код, компилируемый в Wasm. Во втором репозитории лежит простое приложение на Angular, в котором при помощи функций Wasm исследуются некоторые любопытные свойства простых чисел.

В индексе репозитория с AssemblyScript содержится 3 функции для работы с простыми числами:

  • isPrime – Определить, является ли данное целое число простым
  • findFirstNPrimes – Найти первые N простых чисел, где N – целое число
  • optimizedSieve – Найти все простые числа менее N, где N — целое число

AssemblyScript долбавляет в package.json скрипты, генерирующие, соответственно, debug.wasm и release.wasm.

Я скопировала release.wasm в каталог assets приложения Angular, написала загрузчик WebAssembly, чтобы организовать потоковую передачу двоичного файла и вернуть экземпляр WebAssembly. Главный компонент связывает экземпляр с компонентами, где он выступает в качестве ввода. Эти компоненты используют экземпляр для выполнения Wasm и вспомогательных функций для получения результатов операций над простыми числами.

Пишем WebAssembly на AssemblyScript


AssemblyScript — это TypeScript-подобный язык, на котором можно писать код, в дальнейшем компилируемый в WebAssembly.

Начинаем новый проект

npm init

Устанавливаем зависимость

npm install --save-dev assemblyscript

Выполняем команду для добавления скриптов в package.json и файлов скаффолда

npx asinit .

Наши собственные скрипты для генерации файлов debug.wasm и release.wasm

"scripts": {
    "asbuild:debug": "asc assembly/index.ts --target debug --exportRuntime",
    "asbuild:release": "asc assembly/index.ts --target release --exportRuntime",
    "asbuild": "npm run asbuild:debug && npm run asbuild:release",
    "start": "npx serve ."
}

Реализуем алгоритм для работы над простыми числами на AssemblyScript

// assembly/index.ts

// Запись в файле, в этой записи указан модуль WebAssembly

// импорт модуля
declare function primeNumberLog(primeNumber: i32): void;

export function isPrime(n: i32): bool {
  if (n <= 1) {
    return false;
  } else if (n === 2 || n === 3) {
    return true;
  } else if (n % 2 === 0 || n % 3 === 0) {
    return false;
  }

  for (let i = 5; i <= Math.sqrt(n); i = i + 6) {
    if (n % i === 0 || n % (i + 2) === 0) {
      return false;
    }
  }

  return true;
} 

export function findFirstNPrimes(n: i32): Array<i32> {
  let primes = new Array<i32>(n);

  for (let i = 0; i < n; i++) {
    primes[i] = 0;
  }
  primes[0] = 2;
  primeNumberLog(primes[0]);

  let num = 3;
  let index = 0;
  while(index < n - 1) {
    let isPrime = true;

    for (let i = 0; i <= index; i++) {
      if (num % primes[i] === 0) {
        isPrime = false;
        break;
      }
    }

    if (isPrime) {
      primeNumberLog(num);
      primes[index + 1] = num;
      index = index + 1;
    }
    num = num + 2;
  }

  return primes;
}

const MAX_SIZE = 1000001;

export function optimizedSieve(n: i32): Array<i32> {
  const isPrime = new Array<bool>(MAX_SIZE);
  isPrime.fill(true, 0, MAX_SIZE);

  const primes = new Array<i32>();
  const smallestPrimeFactors = new Array<i32>(MAX_SIZE);
  smallestPrimeFactors.fill(1, 0, MAX_SIZE);

  isPrime[0] = false;
  isPrime[1] = false;

  for (let i = 2; i < n; i++) {
    if (isPrime[i]) {
      primes.push(i);

      smallestPrimeFactors[i] = i;
    }

    for (let j = 0; j < primes.length && i * primes[j] < n && primes[j] <= smallestPrimeFactors[i]; j++) {
      const nonPrime = i * primes[j];
      isPrime[nonPrime] = false;
      smallestPrimeFactors[nonPrime] = primes[j];
    }
  }

  const results = new Array<i32>();
  for (let i = 0; i < primes.length && primes[i] <= n; i++) {
    results.push(primes[i]);
  }

  return results;
}

primeNumberLog — это внешняя функция, которая логирует простые числа в findFirstNPrimes. У этой функции нет тела, и именно приложение Angular отвечает за то, чтобы предоставить для неё детали реализации.

После выполнения скрипта npm run asbuild в каталоге builds/ будут содержаться файлы debug.wasm и release.wasm. Та часть работы, которая касается WebAssembly, уже готова, и далее мы будем работать только с приложением Angular.

Комбинируем сильные стороны WebAssembly и Angular


В WebAssembly не переносятся такие высокоуровневые типы данных, как, например, массивы и булевы значения. Поэтому я установила загрузчик assemblyscript и с помощью содержащихся в нём вспомогательных функций привожу к корректным типам значения, возвращаемые функциями Wasm.

Установка зависимости

npm i @assemblyscript/loader

Сборка загрузчика WebAssembly

Далее, методом проб и ошибок я смогла импортировать в приложение Angular функции Wasm, организовав потоковую передачу release.wasm при помощи загрузчика assemblyscript.

src
├── assets
 │   └── release.wasm
├── favicon.ico
├── index.html
├── main.ts
└── styles.scss

Я инкапсулировала загрузчик WebAssembly в загрузочный сервис, тем самым позволив всем компонентам Angular многократно использовать функционал потоковой передачи данных. Если в браузере поддерживается функция instantiateStreaming, то возвращается экземпляр WebAssembly. Если функция instantiateStreaming не поддерживается, то будет вызвана резервная функция. Резервная функция преобразует ответ в массив ArrayBuffer и собирает экземпляр WebAssembly.

DEFAULT_IMPORTS также предоставляет реализацию primeNumberLog. primeNumberLog объявляется в файле index.ts репозитория AssemblyScript. Следовательно, ключом объекта служит его позиция в индексе без файлового расширения.

// web-assembly-loader.service.ts

import { Injectable } from '@angular/core';
import loader, { Imports } from '@assemblyscript/loader';

const DEFAULT_IMPORTS: Imports = { 
  env: {
    abort: function() {
      throw new Error('Abort called from wasm file');
    },
  },
  index: {
    primeNumberLog: function(primeNumber: number) {
      console.log(`primeNumberLog: ${primeNumber}`);
    }
  }
}

@Injectable({
  providedIn: 'root'
})
export class WebAssemblyLoaderService {
  async streamWasm(wasm: string, imports = DEFAULT_IMPORTS): Promise<any> {
    if (!loader.instantiateStreaming) {
      return this.wasmFallback(wasm, imports);
    }

    const instance = await loader.instantiateStreaming(fetch(wasm), imports);
    return instance?.exports;
  }

  async wasmFallback(wasm: string, imports: Imports) {
    console.log('using fallback');
    const response = await fetch(wasm);
    const bytes = await response?.arrayBuffer();
    const { instance } = await loader.instantiate(bytes, imports);

    return instance?.exports;
  }
}


Связываем экземпляр WebAssembly с компонентами Angular

В AppComponent я потоком передала release.wasm, чтобы собрать экземпляр WebAssembly. Затем я связала этот экземпляр с выводом из Angular Components.

// app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: () => inject(PlatformLocation).getBaseHrefFromDOM(),
    }
  ]
};
// full-asset-path.ts

export const getFullAssetPath = (assetName: string) => {
    const baseHref = inject(APP_BASE_HREF);
    const isEndWithSlash = baseHref.endsWith('/');
    return `${baseHref}${isEndWithSlash ? '' : '/'}assets/${assetName}`;
}
// app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule, IsPrimeComponent, FindFirstNPrimesComponent, OptimizedSieveComponent],
  template: `
    <div class="container outer" style="margin: 0.5rem;">
      <h2>Angular + WebAssembly Demo</h2>
      <app-is-prime [instance]="instance" />
      <app-find-first-nprimes [instance]="instance" />
      <app-optimized-sieve [instance]="instance" />
    </div>
  `,
})
export class AppComponent implements OnInit {
  instance!: any;
  releaseWasm = getFullAssetPath('release.wasm');
  wasmLoader = inject(WebAssemblyLoaderService);

  async ngOnInit(): Promise<void> {
    this.instance = await this.wasmLoader.streamWasm(this.releaseWasm);
    console.log(this.instance);
  }
}

Применение WebAssembly к Angular Components

IsPrimeComponent вызывает функцию isPrime, определяющую, является ли данное целое число простым. isPrime возвращает 1, если перед нами простое число, в противном случае возвращает 0. Следовательно, оператор === сравнивает целочисленные значения, чтобы в результате вернуть булево.

// is-prime.component.ts

@Component({
  selector: 'app-is-prime',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form>
      <label for="primeNumber">
        <span>Input an positive integer: </span>
        <input id="primeNumber" name="primeNumber" type="number"
          [ngModel]="primeNumber()" (ngModelChange)="primeNumber.set($event)" />
      </label>
    </form>
    <p class="bottom-margin">isPrime({{ primeNumber() }}): {{ isPrimeNumber() }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IsPrimeComponent {
  @Input({ required: true })
  instance!: any;

  primeNumber = signal(0);

  isPrimeNumber = computed(() => { 
    const value = this.primeNumber();
    return this.instance ? this.instance.isPrime(value) === 1 : false
  });
}


FindFirstNPrimesComponent вызывает функцию findFirstNPrimes, чтобы получить первые N простых чисел. Функция findFirstNPrimes не может перенести целочисленный массив, поэтому я пользуюсь вспомогательной функцией __getArray, имеющейся в загрузчике, и с её помощью преобразую целочисленное значение в корректный целочисленный массив.

// find-first-nprimes.component.ts

@Component({
  selector: 'app-find-first-nprimes',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form>
      <label for="firstNPrimeNumbers">
        <span>Find first N prime numbers: </span>
        <input id="firstNPrimeNumbers" name="firstNPrimeNumbers" type="number"
          [ngModel]="firstN()" (ngModelChange)="firstN.set($event)" />
      </label>
    </form>

    <p class="bottom-margin">First {{ firstN() }} prime numbers:</p>
    <div class="container first-n-prime-numbers bottom-margin">
      @for(primeNumber of firstNPrimeNumbers(); track primeNumber) {
        <span style="padding: 0.25rem;">{{ primeNumber }}</span>
      }
    <div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FindFirstNPrimesComponent {
  @Input({ required: true })
  instance!: any;

  firstN = signal(0);

  firstNPrimeNumbers = computed(() => {
    const value = this.firstN();
    if (this.instance) {
      const { findFirstNPrimes, __getArray: getArray } = this.instance;
      return getArray(findFirstNPrimes(value));
    }

    return [];
  });
}

OptimizedSieveComponent вызывает функцию optimizedSieve, чтобы получить все простые числа, которые меньше to obtain N. Функция optimizedSieve также не может перенести целочисленный массив, и я пользуюсь вспомогательной функцией __getArray, с её помощью преобразую целочисленное значение в корректный целочисленный массив.

// optimized-sieve.component.ts

@Component({
  selector: 'app-optimized-sieve',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form>
      <label for="primeNumber">
        <span>Input an positive integer: </span>
        <input id="primeNumber" name="primeNumber" type="number"
          [ngModel]="lessThanNumber()" (ngModelChange)="lessThanNumber.set($event)" />
      </label>
    </form>

    <p class="bottom-margin">Prime numbers less than {{ lessThanNumber() }}</p>
    <div class="container prime-numbers-less-than-n bottom-margin">
      @for(primeNumber of primeNumbers(); track primeNumber) {
        <span style="padding: 0.25rem;">{{ primeNumber }}</span>
      }
    <div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedSieveComponent {
  @Input({ required: true })
  instance!: any;

  lessThanNumber = signal(0);

  primeNumbers = computed(() => { 
    const value = this.lessThanNumber();
    if (this.instance) {
      const { optimizedSieve, __getArray: getArray } = this.instance;
      return getArray(optimizedSieve(value));
    }

    return [];
  });
}

Готовый пример показан на следующей странице:

railsstudent.github.io

Вот и всё. Надеюсь, вам понравился этот пост, и он вдохновит вас на изучение Angular и других технологий.

Ресурсы:

Теги:
Хабы:
Всего голосов 13: ↑13 и ↓0+13
Комментарии1

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия