/DomainsComparisonTry the search

Same app,
three architectures.

This site recreates foldkit-domains, an Elm-style app built with Foldkit and Effect, using Solid 2.0 primitives. Foldkit Plus, a set of packages on top of Foldkit by this site’s author, ports the same app a second way. All three have the same search, filters, and cart over the same sample data, on Cloudflare Workers. Here is what changed, measured, and where they are not like for like.

JavaScript shipped, against FoldkitSolid 2.02.7× smallerFoldkit Plus25% larger
to priced Top Results, against FoldkitSolid 2.03.1× fasterFoldkit Plus3.1× faster
lines of application code, against FoldkitSolid 2.019% fewerFoldkit Plus11% fewer

Speed

Median (and interquartile range) of 15 cold loads of /?q=sunbeam per site, rotating between sites, measured 2026-11-02. Lower is better. Loads a server failed are not timed: Solid 2.0: 0 of 15 loads failed; Foldkit: 0 of 15 loads failed; Foldkit Plus: 0 of 15 loads failed.
MetricSolid 2.0FoldkitFoldkit PlusAgainst Foldkit
Time to first byteServer response startsNote: Not the same work: Solid’s and Foldkit Plus’s servers render priced rows; Foldkit’s renders a shell without prices. Foldkit Plus’s server reads every price the page shows, then renders, before its first byte, unless it serves a copy it cached for 60 s (Cache-Control: public, max-age=60); the benchmark requests one URL every time, so these are the loads that succeeded, mostly of a page requested again.
71 msIQR 62–82
55 msIQR 48–67
81 msIQR 58–277
Solid 2.0 within run-to-run variationFoldkit Plus within run-to-run variation
First paintFirst contentful paint
188 msIQR 184–216
176 msIQR 168–200
192 msIQR 168–368
Solid 2.0 within run-to-run variationFoldkit Plus within run-to-run variation
Top Results pricedAll four cards show a priceNote: Solid’s and Foldkit Plus’s prices are in the first HTML; Foldkit’s arrive over RPC after its bundle loads. Counted at the later of DOM insertion and first paint.
188 msIQR 184–216
589 msIQR 576–633
192 msIQR 168–368
Solid 2.0 3.1× fasterFoldkit Plus 3.1× faster
First 60 names priced“N of 556 domains checked”, N ≥ 60Note: Foldkit and Solid count in 60-name pages, so this is their “60 of 556”. Foldkit Plus reads the rows in view instead; its server’s first count is now 24 (the rows its window draws), already in the HTML, and the page never counts 60 without scrolling, so it has no value here.
213 msIQR 206–246
638 msIQR 627–686
not measuredSolid 2.0 3.0× fasterFoldkit Plus not measured the same way
New searchKeystroke to four prices, incl. 150 ms debounceNote: A new query each run, so no cache helps.
215 msIQR 212–227
246 msIQR 245–261
307 msIQR 297–315
Solid 2.0 13% fasterFoldkit Plus 25% slower

Size

Compressed size of every file the search page loads, as served with brotli. Lower is better.
MetricSolid 2.0FoldkitFoldkit PlusAgainst Foldkit
JavaScriptEvery script the page loads, brotliNote: Foldkit ships its DevTools overlay in production by design: about 22.9 KB of its total (an estimate; it is in the same bundle as the app). Foldkit Plus ships the same DevTools, and its packages’ code a page that only reads never runs (Remote’s writes, outbox and live modules).
79.3 KB
214.9 KB
268.4 KB
Solid 2.0 2.7× smallerFoldkit Plus 25% larger
HTMLDocument for ?q=sunbeam, brotliNote: Not the same content: Solid’s and Foldkit Plus’s include the priced rows, their data for hydration, and the forms that make filters and the cart work without JavaScript. Foldkit Plus’s no longer carries its stylesheet inline (it links a file since v0.38.0).
5.4 KB
4.5 KB
11.1 KB
Solid 2.0 20% largerFoldkit Plus 2.5× larger
CSSStylesheets the page loads, brotliNote: Solid’s Inspector styles load only when it opens, so they are not in this total. Foldkit Plus links its stylesheet as a file since v0.38.0 (it was inline in the HTML before).
8.9 KB
8 KB
18.1 KB
Solid 2.0 11% largerFoldkit Plus 2.3× larger
HTML and CSSThe two rows above together
14.3 KB
12.5 KB
29.3 KB
Solid 2.0 14% largerFoldkit Plus 2.3× larger

PageSpeed Insights

Google’s Lighthouse lab test (version 13.6.0) of /?q=sunbeam on each live site, run 2026-10-31 (Foldkit Plus 2026-11-02). A coarser instrument than the tables above: read the notes below before comparing scores.

Mobile

Emulated Moto G Power, slow 4G
Solid 2.0all 3 runs
Foldkit3 runs, 93–98
Foldkit Plus2 runs, 73–95
Mobile Lighthouse metrics, median of the runs per site; lower is better.
MetricSolid 2.0FoldkitFoldkit Plus
FCPFirst Contentful Paint0.9 s0.8 s0.9 s
LCPLargest Contentful Paint0.9 s0.8 s1.0 s0.9–1.1 s
TBTTotal Blocking Time50 ms20–50 ms260 ms100–310 ms205 ms180–230 ms
CLSCumulative Layout Shift0.0000.0000.000–0.0820.3230.008–0.637
SISpeed Index0.9 s0.9–2.4 s1.6 s1.5–1.6 s3.6 s3.2–4.1 s

Median of 3 Solid 2.0, 3 Foldkit, 2 Foldkit Plus runs, with the range when runs differed.

Desktop

Emulated desktop, light throttling
Solid 2.0all 3 runs
Foldkit3 runs, 64–74
Foldkit Plus3 runs, 61–90
Desktop Lighthouse metrics, median of the runs per site; lower is better.
MetricSolid 2.0FoldkitFoldkit Plus
FCPFirst Contentful Paint0.2 s0.2 s0.3 s
LCPLargest Contentful Paint0.2 s0.2 s0.3 s
TBTTotal Blocking Time10 ms10–20 ms130 ms130–300 ms240 ms150–310 ms
CLSCumulative Layout Shift0.0000.7710.7870.000–0.838
SISpeed Index0.3 s0.2–0.3 s0.7 s0.7–0.8 s1.5 s1.2–1.7 s

Median of 3 Solid 2.0, 3 Foldkit, 3 Foldkit Plus runs, with the range when runs differed.

  • Lab data, not real users. None of the three sites has enough traffic for Chrome UX Report field data, so PSI shows no Core Web Vitals assessment for any; these are single synthetic loads.
  • Simulated, not observed. Mobile emulates a Moto G Power on slow 4G with a slowed CPU; desktop uses lighter throttling. Lighthouse estimates throttled timings from an unthrottled load, so they are modeled values, and they run from Google’s servers, not the machine the other tables were measured on.
  • Coarse and noisy. PSI shows times rounded to 0.1 s, so paint differences under about 100 ms do not register, and the score moves several points between identical runs (Foldkit mobile ranged 93–98). Medians of three runs per site and form factor are shown, with the range; Foldkit Plus’s mobile median is of two.
  • Foldkit Plus’s runs were taken 2026-11-02 with v0.37.0 and were not repeated for v0.38.0 (PageSpeed Insights’ daily quota for keyless requests was spent), so its caching, linked stylesheet and layout-shift changes are not reflected in them. To get three good runs, eight were read on mobile and five on desktop. In six on mobile and two on desktop, Lighthouse loaded Cloudflare’s error 1102 page (its server ran out of CPU) and scored that page; those reports are linked apart and left out. Mobile stopped at two good runs of eight.
  • Foldkit Plus’s layout shift (0.787 and 0.838 in two of three desktop runs, 0.637 in one mobile run) is most of its score gap where it occurs; its cause is not investigated here.
  • Foldkit’s desktop layout shift (0.771) is its results section moving when the browser renders the fetched results into the server-sent shell; it is most of the gap in its desktop score. Layout shift is 25% of the score.
  • Solid’s runs were taken after a fix and Foldkit’s earlier the same day. Solid’s first runs showed a 0.049 mobile layout shift: the server rendered the result list in the desktop grid and hydration re-laid it on phones. Rows are now placed by CSS container queries, and the shift is 0.
  • Total Blocking Time is main-thread work from JavaScript, so it follows bundle size: Foldkit ships about 2.7 times the JavaScript, including its DevTools overlay.
  • Each tested URL carries an extra run=N query parameter so PSI cannot reuse a cached report; all three apps ignore it.

Code

Counted the same way for both: non-blank lines, excluding tests, fixtures, Story files, and the shared 556-entry TLD list. Solid’s count also excludes this comparison page, which Foldkit has no equivalent of.
MetricSolid 2.0FoldkitFoldkit PlusAgainst Foldkit
Application codeNon-blank TS/TSX lines, excluding testsNote: Solid’s includes its request Inspector (186 lines); Foldkit’s DevTools is a library, not counted (about 4,204 lines of published JavaScript). Foldkit Plus leans on its own packages, also not counted: Remote for prices, Site for the route, Mirror for the address and the cart’s cookie, SSR, and Mixins for the WindowList, dropdowns and sheets.
2,664 lines
3,280 lines
2,928 lines
Solid 2.0 19% fewerFoldkit Plus 11% fewer
Style codeNon-blank lines: CSS, or style.ts for Foldkit PlusNote: Solid and Foldkit write CSS on a Tailwind base. Foldkit Plus has no CSS file: its look is style.ts, TypeScript written with foldkit-mixins’ Style and a theme, so it is code, and it is counted here, not above. Solid’s includes the Inspector’s 78 lines.
933 lines
710 lines
1,126 lines
Solid 2.0 31% moreFoldkit Plus 59% more

What is not apples to apples

  • Different work per page. Solid and Foldkit Plus render prices on the server; Foldkit renders a shell and fetches prices in the browser. First byte, HTML size, and “prices on screen” all reflect that choice, not just framework speed.
  • Foldkit Plus’s server is expensive. Reading the prices a page shows before rendering it costs 150–530 ms of Worker CPU (measured on v0.37.0), mostly Foldkit’s experimental server re-parsing its own output. Workers’ free plan allows 10 ms, so a render that exceeds it fails with error 1102 (a 503). Since v0.38.0 a page is cached for 60 s, so the failure rate depends on whether a copy exists. A page requested again: 0 of 15 of the benchmark’s loads failed, 5 of 15 sequential requests to the same URL straight after, and 15 of 15 minutes later, when no copy was left and no render succeeded. A never-seen search: 13 of 15 failed. Uncached renders can exceed the free Workers plan’s CPU limit because Foldkit’s server checks re-parse the output (an upstream Foldkit issue). Its timings are of the loads that succeeded, of a URL requested again. Solid’s server spends 5–12 ms on the same page.
  • Foldkit ships its DevTools overlay in production by design: an estimated 22.9 KB of its JavaScript, so without it Foldkit would load about 192 KB. Foldkit Plus loads them in development only. Solid’s dev toolbar (@solidjs/start-devtools) is development-only by design and is not in production builds; this app’s own request Inspector is app code, counted in its lines and loaded on demand.
  • Before hydration: Foldkit’s bootstrap script makes the input fully work before its bundle runs. Solid’s captures clicks and typing and replays them when it hydrates. Foldkit Plus keeps what is typed and replays it at boot; nothing on screen answers it until then.
  • Foldkit Plus is not a full match for the original yet: 40 of upstream’s 67 Playwright tests pass against it in Chromium. Most of the rest test by design what it does differently: they expect the page to answer typing before its bundle (11), wait in the browser for prices its server already read (10), or count 60-name pages where it reads the rows in view (6).
  • Hosting: Foldkit is served from a custom domain on a Cloudflare zone, Solid and Foldkit Plus from workers.dev. All three are Cloudflare Workers, but zone settings (protocols, caching features) and plans can differ.
  • Sizes are what the server sends when asked for brotli. Chrome actually receives zstd from Cloudflare; the absolute numbers differ slightly, the comparison does not.
  • One machine and network, desktop Chrome headless at 1280×900, no CPU or network throttling. Phone-class devices are not measured.
  • Timings are medians with the interquartile range across runs; differences whose ranges overlap are reported as within run-to-run variation.

How each one does it

ConcernFoldkit + EffectFoldkit PlusSolid 2.0
StateOne immutable Model, an Effect Schema struct holding everything (route, filters, query cache, list, dialogs).One Model, as in Foldkit, holding the box, filters, layout and cart. Prices live in it too, in Remote’s normalized store keyed by domain name.State lives where it is used: signals, stores, and memos owned by components and providers.
ChangeTyped Messages into a pure update function, composed from reusable Steps.Typed Messages into a pure update, as in Foldkit. The address is a mirror of the Model (Mirror.url), not a Command per keystroke.Event handlers write signals; mutations are actions with optimistic writes.
Side effectsCommands: Effect programs the runtime executes and interrupts.Commands, as in Foldkit, for what the app does itself. Price reads are not Commands: Remote derives them from what the page declares it shows.Server functions plus split effects (compute → apply) for DOM bridges.
Async dataA KeyedQuery block in the Model; AsyncData values folded into a SearchViewState union.Declared Surfaces (Top Results, the rows in view, the whole catalog for the Available filter and price sorts) name the domains they need; Remote reads the ones it lacks in one batch and keeps them fresh for a minute.A router query() keyed by label and range, read with plain createMemo; <Loading> and <Errored> handle the rest.
Progressive loadingA planner reads the VirtualList viewport and schedules the next candidate range.No planner: WindowList names the rows in view (and two past each end), and that Surface reads exactly those names, not the next 60-name page.No planner: each mounted row asks for its own page, so scrolling is the schedule.
CancellationQuery generations reject stale completions; interrupted Commands abort RPCs.A 150 ms debounce (from foldkit-primitives) settles the label the reads use; Remote aborts a read nothing needs any more.None: query() never aborts. Lookups wait until the search is stable for 150 ms, so typing a word sends requests for the final query only.
Server renderingPrerendered and request-time SSR of the shell; prices load in the browser over RPC.Every page rendered whole by the Worker (foldkit-ssr). The server reads the prices the page shows first, and sends them with the page, so the browser asks for none of them again. Each render costs about 60 ms of CPU for the home page and 190 ms for a search, most of it Foldkit’s own checks of the server markup.Streaming SSR with an app-level router preload (a loader): the first ranges are fetched before render, rows read them synchronously, and the browser adopts the data.
Before hydrationA bootstrap script keeps the input usable and synced to the URL before the bundle runs.rh.KeepTyped(): what is typed before boot is kept, with the caret, and replayed when the runtime starts. Nothing answers it before then.The server HTML paints first and hydration starts on the next frame; HydrationScript captures clicks and typing in between and replays them.
Without JavaScriptThe server sends the page shell; prices, results, filters, and the cart need the bundle. With JavaScript off, a search shows no prices.Search, filters, paging, and the cart work: prices are in the HTML, search and filters are a GET form, the list pages by ?from= links, and a cart button is a form posting its Message, which the server runs through update and answers with a redirect.Search, filters, paging, and the cart work: rows are priced in the HTML, filters are a GET form with Previous and Next links, and cart buttons post to a server function that redirects back. CSS `@media (scripting: none)` shows these controls only when scripts are off.
Virtual list@foldkit/ui VirtualList with measured, dynamic row heights.WindowList from foldkit-mixins-ui in the page’s own scroll; rows not yet drawn are sized as the ones measured.Hand-rolled window virtualization with fixed row heights per breakpoint (long names are truncated).
BackendEffect RPC with shared Schemas, typed failures, and request bounds.Remote’s server (serveFetch) at /remote in the Worker: reads by domain name, each price made from the same sample data."use server" functions (RPC at /_server; searches are cacheable GET() reads) plus a small REST API in middleware.
CartIn the Model; instant, and in-memory only.In the Model, kept in a cookie through Mirror.cookie, so it survives reloads; the server prices its names again on every read. One cart with scripts or without.Optimistic action saved through a server function into a cookie, so it survives reloads. Each button is also a form posting to a server function, so the cart works without JavaScript.
StylesTailwind 4 and a stylesheet file.No CSS to write: style.ts, one foldkit-mixins theme and Style code, compiled at build into a hashed stylesheet the page links.Tailwind 4 and a stylesheet file; the Inspector’s and this page’s styles load with them.
ToolingShipped DevTools: Message, Model, Command, and Mount timeline with inspect mode.Foldkit’s DevTools in development; the deployed build leaves them out.A request inspector: the request log, ranges loaded, and injected latency and failures.
DeployAlchemy · Cloudflare.Website.FoldkitWrangler · a Worker with static assetsAlchemy · Cloudflare.Website.Vite

What Foldkit Plus changes over Foldkit

The same app on the same framework, rebuilt with the Foldkit Plus packages. Each change, what it buys, and what it costs.

ChangeWhat it buysWhat it costs
Server data is declared, not fetched (foldkit-remote).Each part of the page names the domains it shows, and Remote reads the ones it lacks in one batch, keeps them fresh and drops what nothing shows. Upstream’s query cache, range planner and generation checks are gone from the app.A runtime of its own in every page, and a new idea to learn (Surfaces and what they read). When a read misbehaves you debug Remote’s state, not your own Command.
The whole page is rendered on the server, data included (foldkit-ssr).Prices arrive in the first HTML and the browser never asks for them again: Top Results are priced in about a third of upstream’s time. The Model travels in the page, so the client starts where the server stopped.Every uncached page costs server CPU (about 60–190 ms here), which the free Workers plan does not allow, and the HTML is larger (11 KB against 4.5 KB) because it carries the Model.
It works without JavaScript.Search, filters, paging and the cart work with scripts off: a GET form, ?from= links, and buttons that post their Message for the server to run through the same update.The server runs the application’s update, so update must stay free of browser-only work, and every control needs a form or link beside its handler.
Reads and pages can be kept at the edge, declared on the domain.The domain says how long a price stays fresh; the server keeps a read or a page only when it can show no viewer would see it differently, so a repeated page skips the render.It helps only while copies exist: on this workers.dev deploy a URL left idle for minutes failed every request, and a never-seen search still renders.
Styles are typed code with a theme (foldkit-mixins), not Tailwind classes.A theme is a few typed knobs (density, radius, contrast, accent), components take their look through named slots, and the build writes one hashed stylesheet.More CSS than Tailwind’s output (18 KB against 8 KB), and the style system’s runtime ships to the browser.
The address and the cart mirror the Model (Mirror.url, Mirror.cookie).No Command per keystroke to keep the URL in step, and the cart survives a reload in a cookie the server reads too.The Model’s shape decides the URL’s; an address that should differ from the state needs a mapping written by hand.
Fewer lines of application code, more library.The app is shorter than upstream’s and has no data-loading code of its own.The most JavaScript of the three, since Remote, the server-rendering client and the style system replace code the app no longer writes.
A young layer on top of Foldkit.Foldkit’s model is kept whole: one Model, a pure update, Commands, the DevTools.Its packages are 0.x and still change between minor versions (0.38.0 changed who may delete in a cascade). It depends on Foldkit’s experimental server rendering, whose checks are most of the render cost.

Where each one shines

Foldkit

  • Every state transition is a pure function, so whole user journeys are tested without a browser.
  • One Model makes the app inspectable and replayable in DevTools.
  • Effect gives typed errors, Schemas shared end to end, and structured cancellation.
  • Measured row heights handle wrapping names; the input works before any JavaScript.

Foldkit Plus

  • Foldkit’s model kept whole: one Model, a pure update, the DevTools.
  • Prices are declared, not fetched by hand: no query cache, range planner, or generation logic in the app (upstream’s domainsQuery.ts, domainPlanning.ts and domainLoading.ts are gone).
  • Prices arrive in the first HTML, and the browser does not ask for them again.
  • It works without JavaScript: search, filters, paging, and the cart, the cart by running the same update on the server.
  • Reads exactly the rows in view; the cart survives reloads in a cookie.

What it costs

  • Its server spends about 60–190 ms of CPU to render a page, so on Workers’ free plan an uncached render can fail with error 1102.
  • The most JavaScript of the three.
  • Typing before the bundle is replayed at boot, not answered.
  • 40 of upstream’s 67 browser tests pass; most of the rest test what it does differently by design.

Solid 2.0

  • Fine-grained updates with no virtual DOM: a cart toggle re-renders one row, not the view.
  • Prices arrive in the first HTML response instead of after the bundle and an RPC.
  • Async reads are plain memos over router queries: no AsyncData union, no loading flags.
  • The cart is a server-checked, optimistic mutation that survives reloads.
  • It works without JavaScript: search, filters, paging, and the cart are server HTML, GET forms, and form posts.

How this was measured

  • All three sites as deployed: domains.foldkit.dev, foldkit-domains-plus.doeixd.workers.dev, and this one, from the same machine and network, in Chrome 156.0.8078.74 (headless) at 1280×900.
  • Each run is a fresh browser context (cold cache), rotating which site goes first. Milestones are recorded with a MutationObserver and counted no earlier than first paint.
  • No backend adds artificial latency. All three apps debounce typing by 150 ms, which is included in the search timing.
  • Sizes: each file the page loaded, fetched again asking for brotli and counted as received.
  • A load the server answered with an error is not timed. It is counted and tried again at once, up to ten times; a site that fails all ten has no timed load in that round. Solid 2.0: 0 of 15 loads failed; Foldkit: 0 of 15 loads failed; Foldkit Plus: 0 of 15 loads failed.
  • Reproduce with node scripts/benchmark.mjs; count lines with node scripts/count-lines.mjs . <foldkit-domains> <domains-plus>, given checkouts of the other two.