Find the cost of adding an npm package to your app's bundle size teardown.kelinci.dev
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: zstd support

Mary f1713269 68dbae36

+69 -25
+1
package.json
··· 13 13 }, 14 14 "dependencies": { 15 15 "@atcute/uint8array": "^1.0.6", 16 + "@bokuweb/zstd-wasm": "^0.0.27", 16 17 "@floating-ui/dom": "^1.7.4", 17 18 "@mary/array-fns": "jsr:^0.1.5", 18 19 "@mary/tar": "jsr:^0.3.2",
+8
pnpm-lock.yaml
··· 11 11 '@atcute/uint8array': 12 12 specifier: ^1.0.6 13 13 version: 1.0.6 14 + '@bokuweb/zstd-wasm': 15 + specifier: ^0.0.27 16 + version: 0.0.27 14 17 '@floating-ui/dom': 15 18 specifier: ^1.7.4 16 19 version: 1.7.4 ··· 164 167 '@babel/types@7.28.6': 165 168 resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} 166 169 engines: {node: '>=6.9.0'} 170 + 171 + '@bokuweb/zstd-wasm@0.0.27': 172 + resolution: {integrity: sha512-GDm2uOTK3ESjnYmSeLQifJnBsRCWajKLvN32D2ZcQaaCIJI/Hse9s74f7APXjHit95S10UImsRGkTsbwHmrtmg==} 167 173 168 174 '@cloudflare/kv-asset-handler@0.4.2': 169 175 resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} ··· 1954 1960 dependencies: 1955 1961 '@babel/helper-string-parser': 7.27.1 1956 1962 '@babel/helper-validator-identifier': 7.28.5 1963 + 1964 + '@bokuweb/zstd-wasm@0.0.27': {} 1957 1965 1958 1966 '@cloudflare/kv-asset-handler@0.4.2': {} 1959 1967
+14 -16
src/components/package-bundle.tsx
··· 2 2 3 3 import { LucideCheck, LucideCircleAlert, LucideInfo, LucideLoader } from '../icons/lucide'; 4 4 import { LRUCache } from '../lib/lru'; 5 - import SizeStat from './size-stat'; 6 5 import { createQuery } from '../lib/query'; 7 6 import { createDerivedSignal } from '../lib/signals'; 8 7 import { progress } from '../npm/events'; ··· 11 10 import Button from '../primitives/button'; 12 11 import * as Dropdown from '../primitives/dropdown'; 13 12 import * as Field from '../primitives/field'; 13 + 14 + import SizeStat from './size-stat'; 14 15 15 16 // #region helpers 16 17 ··· 146 147 return ( 147 148 <div class="flex flex-col gap-5"> 148 149 {/* section header */} 149 - <h3 class="text-base-400 font-semibold text-neutral-foreground-1">Bundle size</h3> 150 + <div class="flex flex-wrap items-baseline justify-between gap-4"> 151 + <h3 class="text-base-400 font-semibold text-neutral-foreground-1">Bundle size</h3> 152 + 153 + {bundle.state === 'refreshing' && ( 154 + <LucideLoader class="size-4 shrink-0 animate-spin-linear text-neutral-foreground-3" /> 155 + )} 156 + </div> 150 157 151 158 {/* subpath selector */} 152 159 <Show when={subpaths.subpaths.length > 1}> ··· 188 195 {(bundleData) => ( 189 196 <div class="flex flex-col gap-5"> 190 197 {/* size display card */} 191 - <div class="flex items-stretch gap-6 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 p-4"> 198 + <div class="flex flex-wrap items-stretch gap-8 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 p-4"> 192 199 <SizeStat label="Minified" size={bundleData().size} /> 193 - <div class="w-px bg-neutral-stroke-3" /> 200 + 194 201 <SizeStat label="Gzip" size={bundleData().gzipSize} /> 195 202 196 - <Show when={bundleData().brotliSize !== undefined}> 197 - <div class="w-px bg-neutral-stroke-3" /> 198 - <SizeStat label="Brotli" size={bundleData().brotliSize!} /> 199 - </Show> 203 + {bundleData().brotliSize! && <SizeStat label="Brotli" size={bundleData().brotliSize!} />} 200 204 201 - <Show when={bundleData().zstdSize !== undefined}> 202 - <div class="w-px bg-neutral-stroke-3" /> 205 + {bundleData().zstdSize !== undefined && ( 203 206 <SizeStat label="Zstd" size={bundleData().zstdSize!} /> 204 - </Show> 205 - <Show when={bundle.state === 'refreshing'}> 206 - <div class="flex items-center"> 207 - <LucideLoader class="size-5 animate-spin-linear text-neutral-foreground-3" /> 208 - </div> 209 - </Show> 207 + )} 210 208 </div> 211 209 212 210 <Switch>
+45 -8
src/npm/lib/bundler.ts
··· 78 78 } 79 79 80 80 /** 81 - * whether zstd compression is supported. 81 + * whether native zstd compression is supported. 82 82 * - `undefined`: not yet checked 83 83 * - `true`: supported 84 - * - `false`: not supported 84 + * - `false`: not supported (will try WASM fallback) 85 85 */ 86 86 let isZstdSupported: boolean | undefined; 87 87 88 88 /** 89 - * get zstd size using compression stream, if supported. 90 - * returns `undefined` if zstd is not supported by the browser. 89 + * zstd-wasm module state. 90 + * - `undefined`: not yet loaded 91 + * - `null`: failed to load 92 + * - module: loaded and ready 93 + */ 94 + let zstdWasm: typeof import('@bokuweb/zstd-wasm') | null | undefined; 95 + 96 + /** 97 + * get zstd-compressed size using WASM fallback. 98 + * returns `undefined` if WASM failed to load. 99 + */ 100 + async function getZstdSizeWasm(code: string): Promise<number | undefined> { 101 + if (zstdWasm === null) { 102 + return undefined; 103 + } 104 + 105 + if (zstdWasm === undefined) { 106 + try { 107 + zstdWasm = await import('@bokuweb/zstd-wasm'); 108 + await zstdWasm.init(); 109 + console.log(`[worker] zstd-wasm initialized`); 110 + } catch { 111 + console.log(`[worker] zstd-wasm failed to load`); 112 + zstdWasm = null; 113 + return undefined; 114 + } 115 + } 116 + 117 + const encoded = new TextEncoder().encode(code); 118 + const compressed = zstdWasm.compress(encoded); 119 + 120 + return compressed.byteLength; 121 + } 122 + 123 + /** 124 + * get zstd size using compression stream if supported, or WASM fallback. 125 + * returns `undefined` if neither native nor WASM is available. 91 126 */ 92 127 async function getZstdSize(code: string): Promise<number | undefined> { 128 + // use WASM fallback if native is known to be unsupported 93 129 if (isZstdSupported === false) { 94 - return undefined; 130 + return getZstdSizeWasm(code); 95 131 } 96 132 97 133 if (isZstdSupported === undefined) { ··· 102 138 isZstdSupported = true; 103 139 return size; 104 140 } catch { 105 - console.log(`[worker] zstd not supported`); 141 + console.log(`[worker] zstd not supported, trying wasm fallback`); 106 142 isZstdSupported = false; 107 - return undefined; 143 + return getZstdSizeWasm(code); 108 144 } 109 145 } 110 146 ··· 256 292 const totalSize = chunks.reduce((acc, c) => acc + c.size, 0); 257 293 const totalGzipSize = chunks.reduce((acc, c) => acc + c.gzipSize, 0); 258 294 const totalBrotliSize = isBrotliSupported ? chunks.reduce((acc, c) => acc + c.brotliSize!, 0) : undefined; 259 - const totalZstdSize = isZstdSupported ? chunks.reduce((acc, c) => acc + c.zstdSize!, 0) : undefined; 295 + const totalZstdSize = 296 + isZstdSupported || zstdWasm != null ? chunks.reduce((acc, c) => acc + c.zstdSize!, 0) : undefined; 260 297 261 298 await bundle.close(); 262 299
+1 -1
vite.config.ts
··· 5 5 export default defineConfig({ 6 6 plugins: [tailwindcss(), solid()], 7 7 optimizeDeps: { 8 - exclude: ['@rolldown/browser'], 8 + exclude: ['@rolldown/browser', '@bokuweb/zstd-wasm'], 9 9 }, 10 10 worker: { 11 11 format: 'es',