Шось про айтішку – Telegram
Шось про айтішку
1.46K subscribers
447 photos
162 videos
2 files
599 links
Фронтенд, ШІ, 3D друк, FPV, історії з життя та роботи
Download Telegram
О динамических стилях и прекомпиляции макросом.

Стили можно прекомпилировать макросом для оптимизации в рантайме. Например трансформировать декларацию в виде мапы в литерал JS объекта, чтобы обойтись без этой трансформации в рантайме: {:top 10} -> {top: 10}.
Статические части стилей можно вообще писать в файл и оставлять только динамические. Обычно 90% стилей статичны, поэтому имеет смысл убрать их из райнтайма:

 10 :color styles/white}

{:color styles/white}
coponent.css -> .component {top: 10}

Можно пойти еще дальше и перенести в статику константные значения типа (def ^:const white "#fff") (обязательно нужен хинт ^:const), тогда такие значение можно определить анализатором (cljs.analyzer) и заинлайнить в статическую часть стилей. Это хороший кейс когда есть набор переиспользуемых цветов, размеров и т.д.

В таком подходе есть минус — записывая в файл на этапе компиляции нет возможности убрать неиспользуемые стили из рантайма.
Неплохой блог о графике. Для погружения в работу с низкоуровневыми графическими API http://vbomesh.blogspot.com/
Повод побывать в Южной Америке https://clojure-south.com/
Хороший пример использования GraalVM (полиглот VM) для рендернига Reagent приложений на сервере (вызов Node.js из JVM) https://nextjournal.com/kommen/react-server-side-rendering-with-graalvm-for-clojure
Опубликовали все записи докладов с Curry On! https://www.youtube.com/channel/UC-WICcSW1k3HsScuXxDrp0w

Интересный доклад о файберах в JVM https://youtu.be/r6P0_FDr53Q
Файберы в кложе 🙌 https://twitter.com/roman01la/status/1153989387211948032?s=09

Ещё перевести на них core.async, чтобы он действительно напоминал гошный CSP.
Небольшой эксперимент с добавлением async/await в ClojureScript https://twitter.com/roman01la/status/1155602189311717377?s=20

В данном случае вместо core.async, который всегда генерирует огромную стейт машину, здесь я добавил в компилятор обработку функций с мета-тегом ^:async, которые компилятором эмитят синтаксис асинхронных функций в JS async function ..., и специальную форму await.

Дальше сгенерированный JS код отдается в Closure Compiler который в зависимости от указанной версии JS на выходе может оставить async/await как есть или трансформировать в генераторы или ту же стейт машину.

Это интересный эксперимент, но в реальном мире добавление нового, платформозависимого синтаксиса только больше навредит языку и разработчикам. async/await нету в Clojure JVM, поэтому сходу страдает портируемость кода. Ну и в целом необходимость в async/await на клиенте — большой вопрос. Часто вам приходиться разруливать нетривиальные асинхронные флоу во фронтенде? async/await именно для этого неплохо подходит, а отправить пару запросов достаточно промисов.
​​На заметку: многие Webpack плагины решаются макросом в Clojure. Например require SVG иконок в инлайновые компоненты.
Через 30 минут вечерний стрим https://www.youtube.com/c/roman01la/live будем работать над патчами в cljs compiler
Интересный эксперимент с перфомансом чтения transit (https://github.com/cognitect/transit-cljs) в cljs.

Для ридера можно переопределять функции трансформации в map и vector. По умолчанию transit использует transient версии этих структур во время десериализации transit. Это уже быстрее, чем персистентые структуры, но если заменить дефолтные хендлеры на transient структуры (transient bean и transient arrayvector) из библиотеки для интеропа данных cljs-bean (https://github.com/mfikes/cljs-bean), то скорость чтения выростает на 30%.

Так выходит потому, что структуры данных в cljs-bean основаны на объектах JavaScript, а их transient версии тупо мутируют эти объекты, поэтому оверхед минимальный.

С сериализацией transit сложнее и она очень медленная :(

В целом если вам не нужно гонять в браузер расширенные типы данных, то лучше использовать JSON.
Недавно отправил патч в cljs compiler который убирает мертвые ветки в if когда тип значения в выражении теста известен заранее (if test then else) https://clojure.atlassian.net/browse/CLJS-2875

Патч нашел пару интересных ошибок в type inference компилятора.

Одна в loop/recur, когда изменяется тип параметра в цикле https://clojure.atlassian.net/browse/CLJS-3158

Вторая в reify, связанная с присвоением некорректного типа https://clojure.atlassian.net/browse/CLJS-3160
Отправил патч в Reagent который ускоряет интерпретацию Hiccup на ~34% https://github.com/reagent-project/reagent/pull/450

Это хороший пример, когда в "горячем коде" стоит использовать нативные методы в обход рефлексии.
В Reagent есть хитрый макрос with-let, который создаёт локальные биндинги в компоненте, но при этом они создаются только один раз, во время инициализации компонента.

Такую же штуку можно сделать макросом с хуком useRef. Например макрос use-let принимает биндинги и разворачивается в обычный let, но значения биндингов оборачиваются в useRef, таким образом в рефе фиксируется только начальное значение.

https://github.com/roman01la/uix/commit/3fdcc82388294812c56d6b866f4a7db276d9d567
В ClojureScript есть хитрый метатег ^not-native, который используется как маркер для генерирования прямого вызова имплементации протокола, в обход рефлексии. Эта оптимизация имеет смысл в «горячем» коде, который часто отрабатывает, например в цикле.

Допустим вот такой пример

(def v [1 2 3])
(nth v 0)
(-nth v 0)
(-nth ^not-native v 0)

В первом случае вызывается функция cljs.core/nth, которая работает не только с IIndexed, но и с JS массивами, строками, и неиндексированными последовательностями.

В данном случае, с вектором, вызов делегируется на фукцию cljs.core/-nth в протоколе IIndexed. То есть зная, что тип данных реализует конкретный протокол можно напрямую вызывать его реализацию (как во втором примере).

Однако такой вызов все равно пройдет через проверку типов, чтобы найти реализацию протокола для конкретного типа данных.

Маркер ^not-native заставит компилятор сгенерировать прямой вызов реализации протокола. То есть вместо чего-то такого cljs.core._nth.invoke(v, 0), будет сгенерирован код типа v.cljs.core.IIndexed_nth(0).

Этот маркер часто применяеться в стандартной библиотеке cljs. В будущем проверки с implements? в коде будут автоматически маркировать проверяемые значения.

(if (implements? IIndexed v)
(-nth v) ;; неявный ^not-native
(nth v))
Недавно нужно было поднять браузерные тесты на CI. В итоге оказалось проще сделать что-то свое, чем разбираться в настройке существующих решений.

Скрипт в 30 строк запускающий тесты Puppeteer https://github.com/roman01la/uix/blob/master/core/noscripts/test.js и конфиг для Circle CI https://github.com/roman01la/uix/blob/master/.circleci/config.yml

profit
В Babel есть два плагина оптимизирующие React компоненты.

Первый инлайнит вызов React.createElement в объект, описывающий ноду в виртуальном DOM, который по сути в рантайме возвращается этим вызовом https://babeljs.io/docs/en/babel-plugin-transform-react-inline-elements

Второй плагин выносит константные React элементы за пределы компонента. Таким образом они создаются только один раз, а не на каждый вызов компонента, но висят в памяти всегда https://babeljs.io/docs/en/babel-plugin-transform-react-constant-elements

Для своей кложурной обертки с Hiccup было несложно сделать инлайнинг элементов, он по сути такой же, как и в том плагине. А с хоистингом элементов вышло интереснее.

В компиляторе ClojureScript есть оптимизация константных значений, сейчас только для keyword и symbol, когда все эти значения выносятся в отдельный неймспейс и по ссылке шарятся в местах использования в коде.

Оказалось несложно включиться в этот механизм и добавить хоистинг константных элементов.

Таким образом имея несколько идентичных кусков Hiccup в разных компонентах и из разных неймспейсов, в скомпилированном JS этот код будет заменен ссылкой на один и тот же элемент.
Недавно наткнулся на минималистичную имплементацию инкрементальных вычислений Adapton https://arxiv.org/abs/1609.05337

Портировал код из пейпера в cljc https://github.com/roman01la/adapton и использую в качестве базы для альтернативы re-frame в UIx https://github.com/roman01la/uix/blob/master/core/src/xframe/core/alpha.cljc

Adapton сам по себе не имеет ничего общего с re-frame и co, но с помощью его примитивов можно строить граф зависимых вычислений (подписок), в котором вершиной будет состояние приложения.

Примитивов там всего два:
Вычисление (thunk) — это функция обернутая в инстанс Adapton ноды.
Adapton ref — мутабельная нода, в которую можно складывать состояние (например как atom).

Таким образом при изменении состояния Adapton инвалидирует зависимые ноды в графе, UIx вызывает ноды на которые подписан UI в данный момент и происходит перевычисление цепочек нод в графе. Важно, что вычисляются только зависимости активных нод в UI. Поэтому ноды которые использутся в других частях приложения, которые в данный момент не отрисованы, не будут обновлены пока не отрисуется необходимый UI.

Так же перевычисление нод не означает, что код в них будет выполняться. Adapton мемоизирует ноды. С другой стороны т.к. там используется простая мемоизация это означает, что кэш в памяти будет расти постоянно. Что не очень хорошо для UI приложений с долгой пользовательской сессией. Нужно прикручивать какой-нибудь LRU кэш.

Другие реализации этого подхода есть на OCaml https://github.com/janestreet/incremental и Rust https://github.com/salsa-rs/salsa

Salsa например используется в компиляторе Rust для инкрементальной компиляции.
Выделил минимальную реализацию async/await в компиляторе cljs в библиотеку https://github.com/roman01la/cljs-async-await

По факту — это расширение анализатора и компилятора cljs, поэтому использовать эту штуку нерекомендуется. К тому же существуют определенные ограничения описанные в секции limitations в readme.

(require '[async-await.core :refer [async await]])

(defn http-get [url]
(async
(let [response (await (js/fetch url))
json (await (.json response))]
(.log js/console json))))
Мы делаем ивент посвященный Clojure, 7-8 декабря, в Киеве. Регистрация уже открыта, больше информации на сайте https://clojureday.in.ua/