Skip to content

Commit 0da76fd

Browse files
captbaritoneclaude
andcommitted
Replace React with Preact via compat layer
Alias react/react-dom to preact/compat in both the Rollup build config and the Vite dev server config. This reduces the gzipped bundle size by ~17% (main bundle) to ~25% (lazy bundle). Fixes Balance and Volume sliders to use onInput instead of onChange, since Preact uses native DOM event semantics where onChange only fires on commit. Extracts snapBalance() helper and forces DOM value sync during drag to preserve the center-snap behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 88ed581 commit 0da76fd

8 files changed

Lines changed: 116 additions & 28 deletions

File tree

packages/webamp-demo/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"serve": "vite preview"
99
},
1010
"devDependencies": {
11+
"@preact/compat": "^18.3.2",
1112
"@sentry/browser": "5.9.1",
13+
"@types/react": "^18.0.26",
14+
"@types/react-dom": "^18.0.10",
1215
"ani-cursor": "workspace:*",
1316
"butterchurn": "3.0.0-beta.5",
1417
"butterchurn-presets": "3.0.0-beta.4",
@@ -19,18 +22,17 @@
1922
"milkdrop-preset-converter-aws": "^0.1.6",
2023
"music-metadata": "^11.6.0",
2124
"music-metadata-browser": "^0.6.1",
25+
"preact": "^10.29.8",
2226
"react": "^19.1.0",
2327
"react-dom": "^19.1.0",
2428
"react-redux": "^8.0.5",
2529
"redux": "^5.0.0-alpha.0",
2630
"redux-sentry-middleware": "^0.1.3",
2731
"redux-thunk": "^3.1.0",
2832
"reselect": "^3.0.1",
33+
"rollup-plugin-polyfill-node": "^0.13.0",
2934
"strtok3": "^10.3.1",
3035
"tinyqueue": "^3.0.0",
31-
"@types/react": "^18.0.26",
32-
"@types/react-dom": "^18.0.10",
33-
"rollup-plugin-polyfill-node": "^0.13.0",
3436
"typescript": "^5.6.2",
3537
"vite": "^5.2.12",
3638
"webamp": "workspace:*",

packages/webamp-demo/vite.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ export default defineConfig({
77
chunkSizeWarningLimit: 2500,
88
},
99
assetsInclude: ["**/*.wsz", "**/*.mp3"],
10+
resolve: {
11+
alias: {
12+
"react/jsx-runtime": "preact/jsx-runtime",
13+
"react/jsx-dev-runtime": "preact/jsx-runtime",
14+
"react-dom/client": "preact/compat/client",
15+
"react-dom": "preact/compat",
16+
react: "preact/compat",
17+
},
18+
},
1019
plugins: [
1120
// Needed for music-metadata-browser which uses polyfillable node APIs
1221
// @ts-expect-error Rollup plugin type mismatch with Vite's stricter types

packages/webamp/js/actionCreators/media.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,19 @@ export function scrollVolume(e: WheelEvent): Thunk {
117117
};
118118
}
119119

120-
export function setBalance(balance: number): Action {
120+
export function snapBalance(balance: number): number {
121121
balance = clamp(balance, -100, 100);
122122
// The balance clips to the center
123123
if (Math.abs(balance) < 25) {
124124
balance = 0;
125125
}
126+
return balance;
127+
}
128+
129+
export function setBalance(balance: number): Action {
126130
return {
127131
type: "SET_BALANCE",
128-
balance,
132+
balance: snapBalance(balance),
129133
};
130134
}
131135

packages/webamp/js/components/Balance.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from "react";
2+
import { useRef } from "react";
23

34
import * as Actions from "../actionCreators";
5+
import { snapBalance } from "../actionCreators/media";
46
import * as Selectors from "../selectors";
57
import { useTypedSelector, useActionCreator } from "../hooks";
68

@@ -15,8 +17,21 @@ export default function Balance({ style, className, id }: Props) {
1517
const setBalance = useActionCreator(Actions.setBalance);
1618
const setFocus = useActionCreator(Actions.setFocus);
1719
const unsetFocus = useActionCreator(Actions.unsetFocus);
20+
const ref = useRef<HTMLInputElement>(null);
21+
22+
const handleInput = (e: Event) => {
23+
const input = e.target as HTMLInputElement;
24+
const snapped = snapBalance(Number(input.value));
25+
// Force DOM value to match snapped value so the slider visually snaps
26+
if (String(snapped) !== input.value) {
27+
input.value = String(snapped);
28+
}
29+
setBalance(snapped);
30+
};
31+
1832
return (
1933
<input
34+
ref={ref}
2035
id={id}
2136
className={className}
2237
type="range"
@@ -25,7 +40,7 @@ export default function Balance({ style, className, id }: Props) {
2540
step="1"
2641
value={balance}
2742
style={{ ...style, touchAction: "none" }}
28-
onChange={(e) => setBalance(Number(e.target.value))}
43+
onInput={handleInput}
2944
onPointerDown={() => setFocus("balance")}
3045
onPointerUp={unsetFocus}
3146
title="Balance"

packages/webamp/js/components/Volume.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function Volume({ id, style, className }: Props) {
2525
value={volume}
2626
style={{ ...style, touchAction: "none" }}
2727
className={className}
28-
onChange={(e) => setVolume(Number(e.target.value))}
28+
onInput={(e) => setVolume(Number((e.target as HTMLInputElement).value))}
2929
onPointerDown={() => setFocus("volume")}
3030
onPointerUp={unsetFocus}
3131
title="Volume Bar"

packages/webamp/package.json

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,7 @@
6161
},
6262
"homepage": "https://github.com/captbaritone/webamp/",
6363
"devDependencies": {
64-
"ani-cursor": "workspace:*",
65-
"butterchurn": "3.0.0-beta.5",
66-
"butterchurn-presets": "3.0.0-beta.4",
67-
"classnames": "^2.2.5",
68-
"invariant": "^2.2.3",
69-
"jszip": "^3.10.1",
70-
"lodash": "^4.17.21",
71-
"music-metadata": "^11.6.0",
72-
"music-metadata-browser": "^0.6.1",
73-
"react": "^19.1.0",
74-
"react-dom": "^19.1.0",
75-
"react-redux": "^8.0.5",
76-
"redux": "^5.0.0-alpha.0",
77-
"redux-thunk": "^3.1.0",
78-
"reselect": "^3.0.1",
79-
"strtok3": "^10.3.1",
80-
"tinyqueue": "^3.0.0",
81-
"winamp-eqf": "workspace:*",
64+
"@rollup/plugin-alias": "^6.0.0",
8265
"@rollup/plugin-commonjs": "^25.0.8",
8366
"@rollup/plugin-json": "^6.1.0",
8467
"@rollup/plugin-node-resolve": "^15.2.3",
@@ -95,18 +78,37 @@
9578
"@types/webaudioapi": "^0.0.27",
9679
"@typescript-eslint/eslint-plugin": "^8.36.0",
9780
"@typescript-eslint/parser": "^8.36.0",
81+
"ani-cursor": "workspace:*",
82+
"butterchurn": "3.0.0-beta.5",
83+
"butterchurn-presets": "3.0.0-beta.4",
84+
"classnames": "^2.2.5",
9885
"data-uri-to-buffer": "^2.0.0",
9986
"imagemin": "^6.1.0",
10087
"imagemin-optipng": "^6.0.0",
88+
"invariant": "^2.2.3",
89+
"jszip": "^3.10.1",
90+
"lodash": "^4.17.21",
91+
"music-metadata": "^11.6.0",
92+
"music-metadata-browser": "^0.6.1",
10193
"postcss": "^8.4.21",
10294
"postcss-import": "^16.1.0",
95+
"preact": "^10.29.8",
10396
"puppeteer": "^22.2.0",
104-
"vitest": "^3.2.0",
97+
"react": "^19.1.0",
98+
"react-dom": "^19.1.0",
99+
"react-redux": "^8.0.5",
100+
"redux": "^5.0.0-alpha.0",
101+
"redux-thunk": "^3.1.0",
102+
"reselect": "^3.0.1",
105103
"rollup": "^4.18.0",
106104
"rollup-plugin-polyfill-node": "^0.13.0",
107105
"rollup-plugin-postcss": "^4.0.2",
108106
"rollup-plugin-visualizer": "^5.12.0",
109-
"typescript": "^5.6.2"
107+
"strtok3": "^10.3.1",
108+
"tinyqueue": "^3.0.0",
109+
"typescript": "^5.6.2",
110+
"vitest": "^3.2.0",
111+
"winamp-eqf": "workspace:*"
110112
},
111113
"prettier": {
112114
"trailingComma": "es5"

packages/webamp/scripts/rollupPlugins.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { nodeResolve } from "@rollup/plugin-node-resolve";
55
import postcss from "rollup-plugin-postcss";
66
import terser from "@rollup/plugin-terser";
77
import { visualizer } from "rollup-plugin-visualizer";
8+
import alias from "@rollup/plugin-alias";
89
import replace from "@rollup/plugin-replace";
910
import postcssOptimizeDataUriPngs from "./postcss-optimize-data-uri-pngs.mjs";
1011
import atImport from "postcss-import";
@@ -13,6 +14,15 @@ import path from "node:path";
1314

1415
export function getPlugins({ minify, outputFile }) {
1516
const plugins = [
17+
alias({
18+
entries: [
19+
{ find: "react/jsx-runtime", replacement: "preact/jsx-runtime" },
20+
{ find: "react/jsx-dev-runtime", replacement: "preact/jsx-runtime" },
21+
{ find: "react-dom/client", replacement: "preact/compat/client" },
22+
{ find: "react-dom", replacement: "preact/compat" },
23+
{ find: "react", replacement: "preact/compat" },
24+
],
25+
}),
1626
replace({
1727
// Ensure we don't use the dev build of React
1828
values: { "process.env.NODE_ENV": JSON.stringify("production") },
@@ -25,14 +35,15 @@ export function getPlugins({ minify, outputFile }) {
2535
browser: true,
2636
preferBuiltins: false,
2737
// Skip deep resolution for better performance
28-
dedupe: ["react", "react-dom"],
38+
dedupe: ["preact"],
2939
}),
3040
// Needed for music-metadata-browser in the Webamp bundle which depends upon
3141
// being able to use some polyfillable node APIs
3242
nodePolyfills(),
3343
typescript({
3444
compilerOptions: {
3545
jsx: "react-jsx",
46+
jsxImportSource: "preact",
3647
module: "esnext",
3748
declaration: false,
3849
declarationDir: undefined,

pnpm-lock.yaml

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)