@samitouri / QOSami-HFS / commits / 2d63e147

saved ~25% of frontend bundle size after switching to preact

Massimo Melina committed Jan 5, 2026 at 13:35 UTC 2d63e1475e537959cf5a93f7d139946bde4a8320
3 files changed +24 -8
frontend/package.json
+2 -2
@@ -11,8 +11,7 @@
11 "dependencies": {
12 "@hfs/shared": "*",
13 "lodash": "^4.17.21",
14 - "react": "^18.3.1",
15 - "react-dom": "^18.3.1",
14 + "preact": "^10.28.1",
15 "react-router-dom": "^6.1.1",
16 "tssrp6a": "^3.0.0",
17 "usehooks-ts": "^2.6.0",
@@ -24,6 +23,7 @@
23 "@types/react": "^18.3.14",
24 "@types/react-dom": "^18.3.2",
25 "@types/xxhashjs": "^0.2.4",
26 + "@preact/preset-vite": "^2.10.2",
27 "@vitejs/plugin-legacy": "^6.0.0",
28 "sass": "^1.54.5",
29 "sonda": "^0.10.1",
frontend/src/toasts.ts
+12 -6
@@ -1,6 +1,7 @@
1 -import { createElement as h, HTMLAttributes, isValidElement, ReactElement, useEffect, useState, useRef,
2 - useCallback, cloneElement, useMemo } from 'react'
3 -import { proxy, useSnapshot } from 'valtio'
1 +import {
2 + createElement as h, HTMLAttributes, isValidElement, ReactElement, useEffect, useState, useRef, useCallback
3 +} from 'react'
4 +import { proxy, ref, useSnapshot } from 'valtio'
5 import { AlertType } from './dialog'
6 import { hIcon, pendingPromise } from './misc'
7 import _ from 'lodash'
@@ -12,7 +13,13 @@ type Content = string | ReactElement
13 export function toast(content: Content, type: ToastType='info', { timeout=5_000 }: { timeout?: number } & Omit<ToastOptions, 'id' | 'content' | 'type'>={}) {
14 console.debug("toast", content)
15 const id = Math.random()
15 - toasts.push({ id, content, type, close })
16 + toasts.push({
17 + id,
18 + // keep react elements out of valtio proxy snapshots
19 + content: isValidElement(content) ? ref(content) : content,
20 + type: isValidElement(type) ? ref(type) : type,
21 + close
22 + })
23 const closed = pendingPromise()
24 setTimeout(close, timeout)
25 return {
@@ -61,7 +68,6 @@ function Toast({ content, type, closed, id, close, ...props }: ToastRecord) {
68 _.remove(toasts, { id })
69 }, [addClass])
70 const ref = useRef<HTMLDivElement | null>()
64 - content = useMemo(() => isValidElement(content) ? cloneElement(content) : content, [content]) // proxied elements are frozen, and crash
71 return h('div', {
72 ...props,
73 ref,
@@ -73,4 +79,4 @@ function Toast({ content, type, closed, id, close, ...props }: ToastRecord) {
79 h('div', { className: 'toast-icon' }, isValidElement(type) ? type : hIcon(type)),
80 h('div', { className: 'toast-content' }, content)
81 )
76 -}
\ No newline at end of file
82 +}
frontend/vite.config.ts
+10
@@ -1,5 +1,6 @@
1 import { defineConfig } from 'vite'
2 import legacy from '@vitejs/plugin-legacy'
3 +import preact from '@preact/preset-vite'
4
5 // https://vitejs.dev/config/
6 export default defineConfig(async () => {
@@ -11,6 +12,14 @@ export default defineConfig(async () => {
12 sourcemap: Boolean(Sonda),
13 //target: "chrome69",
14 },
15 + resolve: {
16 + alias: {
17 + 'react': 'preact/compat',
18 + 'react-dom': 'preact/compat',
19 + 'react-dom/test-utils': 'preact/test-utils',
20 + 'react/jsx-runtime': 'preact/jsx-runtime',
21 + },
22 + },
23 server: {
24 port: 3005,
25 host: '127.0.0.1',
@@ -24,6 +33,7 @@ export default defineConfig(async () => {
33 }
34 },
35 plugins: [
36 + preact(),
37 legacy({
38 renderModernChunks: false, // single version, legacy one
39 polyfills: false, // keeping polyfills at a minimum, manually