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