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