WASM (WebAssembly) или нет во фронтенд-проектах?

Что я хочу сделать ?

Я пытаюсь найти подход к повышению производительности моих внешних проектов.

Мои фронтенд-проекты - это в основном веб-сайты для разных целей.

У меня всегда есть сложная логика, например фильтрация и преобразование ответа большого серверного интерфейса во внешнем интерфейсе. Иногда мне нужно использовать общий ответ Rest, и я не могу ни использовать graphql, ни добавить BFF.

Я добавляю эти сложности:

  • сложные правила UI / UX.
  • поддержка нескольких браузеров.
  • 3G, 4G, …
  • отзывчивость и хороший пользовательский опыт.

Как мой интерфейс может справиться с этими сложностями и продолжать работать!

Некоторые приемы, которые я использую для улучшения своего интерфейса:

  • Использование async для тяжелых задач (в основном генератор ES6).
  • Ленивая загрузка маршрутов, тяжелых модулей и компонентов.
  • Загрузка по запросу модуля, который требовался только при нажатии на кнопку вроде аналитики.
  • Webpack tree-shaking и ES-модули.
  • Группируйте анализируйте часто.
  • Часто проверяйте код с помощью chrome devtools.

Хм, тогда где же во всем этом WASM?

Почему WASM?

WASM делает с Javascript то же, что Assembly делает с языком C: иногда в программе C, когда нам нужно быстрее, мы пишем некоторые части, используя Assembly.

Чем ближе мы к машинному коду, тем быстрее и производительнее будет наше приложение, потому что мы избежим всех промежуточных шагов, необходимых для преобразования кода с языка высокого уровня в машинный.

Иногда написание кода с использованием C может быть быстрее, чем написание кода с использованием сборки напрямую, потому что абстракция позволяет нам оптимизировать, а компилятор C генерирует более оптимизированный код сборки.

Чем больше мы добавляем слоев над машинным кодом, тем большее значение будет иметь время, затрачиваемое на оптимизацию и повышение производительности, и тем сложнее будет наше решение.

У каждого шага преобразования есть свои временные и производственные затраты.

Возвращаясь к JS и особенно к тому, как V8 работает внутри:

Увидев этот рабочий процесс, я сказал себе: дорогие задачи не должны проходить через все эти шаги, но должны быть ближе к машинному коду. Как насчет того, чтобы иметь готовый ByteCode, который можно было бы выполнять напрямую, как при использовании C и Assembly?

И тут в игру вступает WASM!

Могу ли я поручить WASM тяжелые задачи? Насколько это будет производительно?

Что такое WASM?

WebAssembly - это новый тип кода, который можно запускать в современных веб-браузерах, и он предоставляет новые функции и существенное повышение производительности. Он не предназначен для написания вручную, скорее он предназначен для эффективной компиляции для низкоуровневые исходные языки, такие как C, C ++, Rust и т. д.

Https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts



Более теоретические концепции:

он позволяет запускать код, написанный на нескольких языках, в сети со скоростью, близкой к нативной, с клиентскими приложениями, работающими в Интернете, которые раньше не могли этого сделать.

Https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts

Эффективная цель компиляции для низкоуровневых исходных языков, таких как C, C ++, Rust и т. д.. = › WASM оптимизируется на этапе компиляции, что означает, что он готов к выполнению и не требует всего жизненного цикла JS V8.

Это большой шаг! Вот что мне нужно!

Я решил создать проект с использованием React, JS, Rust и WASM и попытаться делегировать WASM некоторые тяжелые задачи внешнего интерфейса, такие как: асинхронные HTTP-запросы, отображение большого ответа, фильтр, редуктор,…!

Проект React-JS-Rust-WASM

Шаблон Rust Webpack

Для создания своего проекта я использовал этот шаблон:



Созданный проект будет предварительно настроен для работы с Rust, JS и webpack (готов к запуску). Вам нужно только добавить свои собственные зависимости (Rust Cargo или JS npm) или изменить конфигурацию веб-пакета по умолчанию, чтобы добавить свои собственные загрузчики и плагины.

Будут созданы три важных репозитория:

  • src: сюда мы поместим наш код Rust.
  • js: сюда мы поместим наш JS-код.
  • pkg: сгенерированные файлы WASM.

Мой проект :



Вызов Rust из Javascript

Ржавчина:

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
    alert("Hello, rust-wasm-frontend!");
}

Javascript:

import React, { Component } from "react";
import {
  Button,
} from "antd";
import {
  TasksUtilsMocker,
}from '../utils'
class UserListPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      wasm: {},
    };
  }
  componentDidMount() {
    this.loadWasm();
  }
  loadWasm = async () => {
    import("../../pkg/index.js")
      .then((wasm) => {
        if (wasm) {
          this.setState({
            wasm,
          });
        }
      })
      .catch(console.error);
  };
  render() {
    const { wasm = {} } = this.state;
    const {
      greet,
    } = wasm || {};
    return (
      <div className="App">
        <header className="App-header">
          <div>
            <div>
              { (greet) && (
                <Button
                  onClick={() => wasm.greet()}
                >
                  Click Me
                </Button>
              ) }
            </div>
          </div>
        </header>
      </div>
    );
  }
}
export default UserListPage;

Rust-WASM против JS

Мне не терпится узнать о производительности WASM!

В моих интерфейсных проектах дорогостоящими задачами являются, в частности, преобразование ответов серверной части в соответствии с потребностями внешнего интерфейса (отображение, фильтрация, сокращение,…)

Я сосредоточусь на этих частях в моем сравнении и тестах, давайте начнем!

Отфильтруйте большой массив, чтобы оставить только числа

Javascript:

 const entries = [
    ...[...Array(40000)].map(e=>~~(Math.random() * 4000)),
 ];
  console.log("entries : ", entries);

  const jsFilterT0 = performance.now();
  const resultJSFilter = entries.filter(item => typeof item === 'number')
  const jsFilterT1 = performance.now();
  console.log("Call to JS filter took " + (jsFilterT1 - jsFilterT0) + " milliseconds.")
  console.log("resultJSFilter : ", resultJSFilter);

Ржавчина:

#[wasm_bindgen]
pub fn collect_numbers(numbers: &JsValue) -> Result<Array, JsValue> {
    let nums = Array::new();
    let iterator = js_sys::try_iter(numbers)?.ok_or_else(|| "need to pass iterable JS values!")?;

   for x in iterator {
    // If the iterator's `next` method throws an error, propagate it
        // up to the caller.
        let x = x?;

        // If `x` is a number, add it to our array of numbers!
        if x.as_f64().is_some() {
            nums.push(&x);
        }
   }

   Ok(nums)
}

Вызов Rust из Javascript:

const {
  collect_numbers,
} = wasm || {};

if(collect_numbers){
  const entries = [
    ...[...Array(40000)].map(e=>~~(Math.random() * 4000)),
  ];
  console.log("entries : ", entries);

  const wasmT0 = performance.now();
  const resultWasmFilter = collect_numbers(entries);
  const wasmT1 = performance.now();
  console.log("Call to Rust collect_numbers took " + (wasmT1 - wasmT0) + " milliseconds.");
  console.log("resultWasmFilter : ", resultWasmFilter);
}

Результат выполнения:

Call to JS filter took 1.304999997955747 milliseconds.
Call to Rust collect_numbers took 44.355000005452894 milliseconds.

Я удивлен, JS справляется с этой задачей быстрее, чем Rust WASM!

Я решил спросить у сообщества Rust, ожидается ли такой результат и на правильном ли я пути:



Ответы:

Вывод моего первого теста: JS работает быстрее, если логика проста, даже если данные большие!

Я решил применить более сложную логику: большой массив объектов, и я проделаю много операций!

Сортировать, отображать, фильтровать большой массив (около 10000 записей)

Большие данные:



Javascript:

const moreBigData = [
  ...MockData,
  ...MockData,
]
const jsMapT0 = performance.now();
const resultJSMap = moreBigData
      .sort((a,b) => a.id - b.id)
      .map(item => ({
            album_id: item.album_id,
            id: item.id,
            value: `id: ${item.id}, value: ${item.title}`,
            title: item.title,
            thumbnail_url: item.thumbnail_url,
            url: item.url,
      }))
      .filter(item => item.id > 200)
const jsMapT1 = performance.now();
console.log("Call to JS map took " + (jsMapT1 - jsMapT0) + " milliseconds.")
console.log("resultJSMap : ", resultJSMap);

Ржавчина:

#[derive(Serialize, Deserialize)]
pub struct Photo {
    albumId: i64,
    id: i64,
    title: String,
    url: String,
    thumbnailUrl: String,
}

#[derive(Serialize, Deserialize)]
pub struct PhotoOutput {
    albumId: i64,
    id: i64,
    title: String,
    url: String,
    thumbnailUrl: String,
    value: String,
}

 #[wasm_bindgen]
 pub fn transform_me(js_objects: &JsValue) -> JsValue {
  let mut elements: Vec<Photo> =   js_objects.into_serde().unwrap();
  elements.sort_by(|a, b| a.id.partial_cmp(&b.id).unwrap());

     let values: Vec<PhotoOutput> = elements
         .iter()
         .map(|e| {
             let _title = &e.title;
             let _thumbnail_url = &e.thumbnailUrl;
             let _url = &e.url;
             let _id = e.id;
             PhotoOutput {
                 albumId: e.albumId,
                 id: _id,
                 value: format!("id: {}, value: {}", _title, _id),
                 title: format!("{}", _title),
                 thumbnailUrl: format!("{}", _thumbnail_url),
                 url: format!("{}", _url),
             }
         })
         .filter(|e| e.id > 200)
         .collect();

     JsValue::from_serde(&values).unwrap()
 }

Вызов Rust из Javascript:

const {
     transform_me
 } = wasm || {};

 if(transform_me){
     const moreBigData = [
       ...MockData,
       ...MockData,
     ]
     const wasmT0 = performance.now();
     const resultWASMMap = transform_me(moreBigData);
     const wasmT1 = performance.now();
     console.log("Call to Rust WASM transform_me took " + (wasmT1 - wasmT0) + " milliseconds.");
     console.log("resultWASMMap : ", resultWASMMap);
}

Результат выполнения:

Call to JS map took 6.070000003091991 milliseconds.
Call to Rust WASM transform_me took 942.639999993844 milliseconds.

Я удивлен, JS также справляется с этой задачей быстрее, чем Rust WASM!

Я решил посоветоваться с сообществом Rust:

Ответы:

Однако обычно на стороне интерфейса мы не выполняем дорогостоящих вычислений, это будет роль Backend или BFF (Backend For Frontend, как graphql) или выделенного микросервиса.

В интерфейсе важны удобство работы и скорость реагирования, поэтому мы не блокируем пользователя дорогостоящими задачами, а используем асинхронный режим и по мере возможности делегируем их бэкенду. У Frontend есть ограниченные ресурсы и множество ограничений (3G, 4G, IE, chrome, firefox, safari, macOS, windows, linux,…)!

Например, я предпочитаю делегировать такие задачи, как рендеринг и генерация PDF, чтобы серверная часть и веб-интерфейс загружали только файл, а не выполняли это на стороне внешнего интерфейса (намного быстрее). То же самое для обработки изображений и видео.

Я стараюсь, чтобы интерфейс был как можно более легким: отображение и отзывчивость!

Вывод моего второго теста: JS работает быстрее, даже если операции с массивами сложные, а массив большой!

Я решил оставить свой JS для всех операций с массивами, попробую асинхронные http-вызовы!

Rust + reqwest vs JS + axios (скачали около 7800 рекоров)

Javascript:

const jsRequestSocials = (wasm) => {
  const {
    get_all_socials,
  } = wasm || {};
  const wasmT0 = performance.now();

  if(get_all_socials){
      axios
          .get('http://localhost:3000/socials', { crossdomain: true })
          .then(data => {
            const wasmT1 = performance.now();
            console.log("jsRequestSocials took " + (wasmT1 - wasmT0) + " milliseconds.");
            console.log('jsRequestSocials data : ', data);
          })
          .catch(error => console.log('jsRequestSocials error : ', error));
  }
};

Ржавчина:

#[derive(Debug, Serialize, Deserialize)]
pub struct Geo {
    pub lat: String,
    pub lng: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Address {
    pub street: String,
    pub suite: String,
    pub city: String,
    pub zipcode: String,
    pub geo: Geo,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Company {
    pub name: String,
    pub catchPhrase: String,
    pub bs: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SocialFriend {
    pub id: i64,
    pub name: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SocialUser {
    pub _id: String,
    pub index: i64,
    pub guid: String,
    pub isActive: bool,
    pub balance: String,
    pub picture: String,
    pub age: i32,
    pub eyeColor: String,
    pub name: String,
    pub gender: String,
    pub company: String,
    pub email: String,
    pub phone: String,
    pub address: String,
    pub about: String,
    pub registered: String,
    pub latitude: f64,
    pub longitude: f64,
    pub greeting: String,
    pub favoriteFruit: String,
    pub friends: Vec<SocialFriend>,
    pub tags: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ServerError {
    pub message: String,
    pub status: i32,
}

#[wasm_bindgen]
pub async fn get_all_socials() -> Result<JsValue, JsValue> {
    let result = reqwest::Client::new()
        .get("http://localhost:3000/socials")
        .send()
        .await;

    match result {
        Ok(result) => {
            let text = result.text().await.unwrap();
            let socials: Vec<SocialUser> = serde_json::from_str(&text).unwrap();
            Ok(JsValue::from_serde(&socials).unwrap())
        }
        Err(_e) => {
            let message = "".to_string();
            let empty_social = ServerError {
                message: message,
                status: 200,
            };
            Ok(JsValue::from_serde(&empty_social).unwrap())
        }
    }
}

Вызов Rust из Javascript:

const wasmRequestSocials = (wasm) => {
    const {
      get_all_socials,
    } = wasm || {};
    const wasmT0 = performance.now();

    if(get_all_socials){
      get_all_socials()
      .then(data => {
        const wasmT1 = performance.now();
        console.log("wasmRequestSocials took " + (wasmT1 - wasmT0) + " milliseconds.");
        console.log('wasmRequestSocials data : ', data);
      })
      .catch(error => console.log('wasmRequestSocials error : ', error));
    }
};

Результат выполнения:

jsRequestSocials took 1896.6450000007171 milliseconds.
wasmRequestSocials took 5983.189999999013 milliseconds.

JS + axios также быстрее, чем Rust + WASM!

Проблема с большим пакетом WASM

Выполнение только этих 6 или 7 функций приведет к получению большого файла WASM: около 2,2 МО, даже если я соберу продукт!

Мне понравилась безопасность при использовании Rust + WASM (все проверяется перед использованием внешнего интерфейса, что может заменить проверки Proptypes), но я потеряю производительность!

Совместимость с браузером WASM



К сожалению, IE не поддерживается, это большое ограничение!

Заключение

Мне понравилась концепция, согласно которой WASM позволяет нам писать в Интернете на другом языке, чем Javascript.

Мне понравилась безопасность Rust и проверка перед использованием интерфейса: мы уверены, что если Rust скомпилируется и запустится, все будет работать отлично!

Однако Javascript, несмотря на многие недостатки, особенно безопасность, Javascript по-прежнему намного быстрее и производительнее!

На данный момент я решил не включать WASM в свои внешние проекты, но я буду продолжать пытаться и наблюдать за его развитием.

Производительность, отзывчивость и совместимость с браузером являются наиболее важными составляющими внешнего интерфейса.

Возможно, WASM даст лучшую производительность, если я буду выполнять некоторые дорогостоящие вычислительные задачи, такие как 3D или VR или обработка изображений / видео во внешнем интерфейсе. Однако иногда такие задачи, как обработка изображений / видео, можно делегировать бэкэнду, фронтенд получит только результат!

Также из соображений качества кода и хорошего дизайна я избегаю смешивания обязанностей:

  • Фронтенд - это пользовательский интерфейс, пользовательский интерфейс и отзывчивость, не более того!
  • Бэкэнд для API.
  • BFF посредник.

Я уверен в производительности JS!

Вы можете найти исходный код проекта здесь:



Спасибо, что прочитали мою историю.

Вы можете найти меня по адресу:

Twitter: https://twitter.com/b_k_hela

Github: https://github.com/helabenkhalfallah