weftsource
- download size
- 3.2 KB
- components re-run
- 0
- small updates run
- 0
- page changes
- 0
- time per update
- –
Write components in TSX, as you would in React. weft compiles them into small, direct page updates, and comes with routing, data, a UI kit and errors your coding agent can act on.
node packages/create-weft/scripts/local.mjs new my-appweft is not on npm yet, so run this from a checkout of the repo.
Here is a small shop, built one idea at a time. Each step adds a few lines, marked in the code, and each one is running.
A weft component is a function that returns TSX. Keep a value in state, show it, change it. There is no new template language to learn.
One product and an add-to-cart button. count is state: a value the page shows and can change.
import { state } from '@weft-js/core'import { Badge, Button, Card } from '@weft-js/warp'export default function Shop() { const count = state(0) return ( <Card title="Coffee, $12"> <Button onClick={() => count.value++}>Add to cart</Button> <Badge count>{count.value}</Badge> </Card> )}Canvas art in your theme's own inks. These run on this page, three at most moving at a time.
Say how a value is worked out, and weft keeps it current. The shop gets a total, and shipping that turns free at $36.
The total and shipping are new, marked in the code. Neither is set by hand: derived works each one out from count.
Shipping $0
Total $0
import { derived, state } from '@weft-js/core'import { Badge, Button, Card } from '@weft-js/warp'export default function Shop() { const count = state(0) const total = derived(() => count.value * 12) const shipping = derived(() => total.value === 0 || total.value >= 36 ? 0 : 4, ) return ( <Card title="Coffee, $12"> <Button onClick={() => count.value++}>Add to cart</Button> <Badge count>{count.value}</Badge> <p>Shipping ${shipping.value}</p> <p><strong>Total ${total.value + shipping.value}</strong></p> </Card> )}Most frameworks re-run your component and compare the result to find what changed. weft works that out when it compiles, so a change goes straight to the piece of the page it affects. The shop gets more products and a search box: one new value and one line of filtering, and everything that depends on it follows.
A search box. shown is derived from what you type, and the filter is one line of ordinary JavaScript.
0
0
0
Shipping $0
Total $0
import { derived, state } from '@weft-js/core'import { Badge, Button, Card, Input } from '@weft-js/warp'const products = [ { id: 1, name: 'Coffee', price: 12, count: state(0) }, { id: 2, name: 'Filters', price: 5, count: state(0) }, { id: 3, name: 'Grinder', price: 40, count: state(0) },]export default function Shop() { const search = state('') const shown = derived(() => { const word = search.value.toLowerCase() return products.filter(p => p.name.toLowerCase().includes(word)) }) const total = derived(() => products.reduce((sum, p) => sum + p.count.value * p.price, 0), ) const shipping = derived(() => total.value === 0 || total.value >= 36 ? 0 : 4, ) return ( <Card title="Shop"> <Input label="Search" value={search.value} onInput={e => (search.value = e.currentTarget.value)} /> {shown.value.map(p => ( <p key={p.id}> <Button size="sm" onClick={() => p.count.value++}> {p.name}, ${p.price} </Button> <Badge count>{p.count.value}</Badge> </p> ))} <p>Shipping ${shipping.value}</p> <p><strong>Total ${total.value + shipping.value}</strong></p> </Card> )}The buttons and fields so far are warp, weft's own component library: 244 components, with keyboard support, themes and 651 icons. The shop gets a checkout.
A checkout form from warp, so the shop now has a labelled field, a switch and a submit button.
0
0
0
Shipping $0
Total $0
import { derived, state } from '@weft-js/core'import { Badge, Button, Card, Input, Switch } from '@weft-js/warp'const products = [ { id: 1, name: 'Coffee', price: 12, count: state(0) }, { id: 2, name: 'Filters', price: 5, count: state(0) }, { id: 3, name: 'Grinder', price: 40, count: state(0) },]export default function Shop() { const search = state('') const ordered = state(false) const shown = derived(() => { const word = search.value.toLowerCase() return products.filter(p => p.name.toLowerCase().includes(word)) }) const total = derived(() => products.reduce((sum, p) => sum + p.count.value * p.price, 0), ) const shipping = derived(() => total.value === 0 || total.value >= 36 ? 0 : 4, ) const order = (e: Event) => { e.preventDefault() ordered.value = true } return ( <Card title="Shop"> <Input label="Search" value={search.value} onInput={e => (search.value = e.currentTarget.value)} /> {shown.value.map(p => ( <p key={p.id}> <Button size="sm" onClick={() => p.count.value++}> {p.name}, ${p.price} </Button> <Badge count>{p.count.value}</Badge> </p> ))} <p>Shipping ${shipping.value}</p> <p><strong>Total ${total.value + shipping.value}</strong></p> <form onSubmit={order}> <Input label="Email for the receipt" type="email" /> <Switch defaultChecked>Gift wrap</Switch> <Button type="submit" disabled={total.value === 0}> {ordered.value ? 'Order placed' : 'Place order'} </Button> </form> </Card> )}A real shop keeps its products in a database. With @weft-js/start, a server query lives beside your components, and each file in src/routes is a page. Database and sign-in are built in, so the whole app is one codebase.
// src/server/shop.tsexport const products = query({ tags: ['products'], run: ({ ctx }) => ctx.db.select().from(schema.products),})// in Shop: the list now comes from the databaseconst list = products(){(list.value ?? []).map(p => <Row key={p.id} product={p} />)}These are three real production builds of one small app, running in your browser now: a counter and a grid of 1,000 cells. Press the buttons and compare what each one had to do.
All three make the same page changes, because they show the same thing. The difference is the work behind it: React re-runs components to find those changes, and ships more code to do so. How this is measured: each update at 60 a second is timed on its own, with nothing watching the page, and the times are added up. One update in 30 runs untimed so its page changes can be counted. Time per update stays empty until a run has about five seconds in it, because a few clicks are too few to compare. On an Apple M3 Pro, 60 updates a second take 107 µs each in weft, 182 µs in Solid, 341 µs in React and 72 µs in hand-written DOM.
weft is faster than Solid in 4 of these 12 rows and slower in 1, marked in red. In the rest the rounds overlap. Times are milliseconds, the median of three rounds on an Apple M3 Pro on 2026-11-01, with Solid 1.9.7.
| Operation | weft | Solid | React | Fastest |
|---|---|---|---|---|
| Create 1,000 rows | 22.9 | 20.6 | 24.2 | Too close |
| Replace 1,000 rows | 22.1 | 22.1 | 27.2 | Too close |
| Create 10,000 rows | 218.9 | 212.8 | 385.3 | Too close |
| Append 1,000 rows | 22.7 | 23.2 | 27.3 | Too close |
| Update every 10th row | 0.61 | 0.66 | 1.07 | Too close |
| Select a row | <0.01 | 0.01 | 0.17 | weft |
| Swap two rows | 0.18 | 0.25 | 2.84 | Too close |
| Remove a row | 0.20 | 0.17 | 0.32 | Solid |
| Clear all rows | 2.67 | 2.67 | 4.11 | Too close |
| Update one cell of 10,000 | <0.01 | <0.01 | 1.07 | weft |
| Update every 10th of 10,000 | 0.62 | 1.07 | 2.31 | weft |
| Update all 10,000 cells | 1.71 | 3.05 | 3.82 | weft |
The same dashboard app, built with a UI kit in each framework. Less JavaScript means pages are ready sooner, most of all on phones and slow connections.
JavaScript sent to the browser, compressed with brotli, from this repo's comparison app.
The compiler stops the mistake before it ships and says how to fix it, in words your agent can act on.
The component works out its total once, so the number on the page can never change.
function Total(props: { count: number }) { const total = props.count * 5 return <b>{total}</b>}function Total(props: { count: number }) { const total = props.count * 5 const total = derived(() => props.count * 5) return <b>{total}</b> return <b>{total.value}</b>}function Total(props: { count: number }) { const total = derived(() => props.count * 5) return <b>{total.value}</b>}Count 2
Press +1. The count goes up, the total stays stuck.
Make an app from a checkout, then work through Learn a chapter at a time.