noreflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/index.cjs +1572 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +161 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +1566 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jbpete87
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Noreflow
|
|
2
|
+
|
|
3
|
+
**Pure TypeScript layout engine. Flexbox + CSS Grid. Zero dependencies. No WASM. No DOM.**
|
|
4
|
+
|
|
5
|
+
Every time an AI chat streams a token, the browser recalculates the entire page layout. That's why ChatGPT stutters, Claude janks, and every streaming UI feels sluggish. The DOM reflow bottleneck is the performance wall every serious web app hits.
|
|
6
|
+
|
|
7
|
+
Noreflow computes layout as a pure function — feed it a tree of nodes with styles, get back exact pixel positions. Pair it with [Pretext](https://pretext.dev) for text measurement and you get **zero-reflow rendering** for streaming UIs, virtual scrolling, Canvas apps, and anywhere else DOM layout is too slow.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install noreflow
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { computeLayout } from 'noreflow';
|
|
19
|
+
|
|
20
|
+
const layout = computeLayout({
|
|
21
|
+
style: { width: 400, height: 200, gap: 16, padding: 20 },
|
|
22
|
+
children: [
|
|
23
|
+
{ style: { flexGrow: 1, height: 60 } },
|
|
24
|
+
{ style: { flexGrow: 2, height: 60 } },
|
|
25
|
+
{ style: { width: 80, height: 60 } },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// layout.children[0] → { x: 20, y: 20, width: 88, height: 60 }
|
|
30
|
+
// layout.children[1] → { x: 124, y: 20, width: 176, height: 60 }
|
|
31
|
+
// layout.children[2] → { x: 316, y: 20, width: 80, height: 60 }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pure data in, pure data out. No classes, no manual memory management, no `.free()`.
|
|
35
|
+
|
|
36
|
+
## The Problem
|
|
37
|
+
|
|
38
|
+
The DOM reflow bottleneck is THE performance wall every serious web app hits. Every app that's gotten big enough has had to build ugly workarounds:
|
|
39
|
+
|
|
40
|
+
- **Slack** estimates message heights and gets them wrong (scroll jumping)
|
|
41
|
+
- **Google Docs** slows down on long documents because every keystroke triggers paragraph reflow
|
|
42
|
+
- **VS Code** built an entire custom editor (Monaco) because `contenteditable` + DOM layout couldn't keep up
|
|
43
|
+
- **Figma** renders entirely to Canvas because DOM layout can't handle a design tool
|
|
44
|
+
- **Every AI chat app** (ChatGPT, Claude) janks when streaming because tokens cause text reflow → height changes → scroll jumps
|
|
45
|
+
|
|
46
|
+
The root cause is the same: the browser's layout engine is a black box that forces synchronous reflow whenever content changes.
|
|
47
|
+
|
|
48
|
+
## The Solution
|
|
49
|
+
|
|
50
|
+
Noreflow + [Pretext](https://pretext.dev) = zero-reflow rendering.
|
|
51
|
+
|
|
52
|
+
1. **Pretext** measures text (line breaks, wrapping, height) as pure arithmetic — no DOM
|
|
53
|
+
2. **Noreflow** computes layout (flexbox, grid, absolute positioning) as a pure function — no DOM
|
|
54
|
+
3. Together they replace the browser's Style → Layout pipeline with deterministic, synchronous TypeScript
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { prepareWithSegments, layout } from '@chenglou/pretext';
|
|
58
|
+
import { computeLayout } from 'noreflow';
|
|
59
|
+
|
|
60
|
+
const prepared = prepareWithSegments(
|
|
61
|
+
'Each new token can cause a line wrap, changing the height.',
|
|
62
|
+
'400 14px Inter',
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const measure = (availableWidth: number) => {
|
|
66
|
+
const result = layout(prepared, availableWidth, 20);
|
|
67
|
+
return { width: availableWidth, height: result.height };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const chatMessage = computeLayout({
|
|
71
|
+
style: { width: 320, flexDirection: 'row', gap: 8, padding: 12 },
|
|
72
|
+
children: [
|
|
73
|
+
{ style: { width: 32, height: 32, flexShrink: 0 } },
|
|
74
|
+
{
|
|
75
|
+
style: { flexGrow: 1, flexDirection: 'column', gap: 4 },
|
|
76
|
+
children: [
|
|
77
|
+
{ style: { height: 16 } },
|
|
78
|
+
{ measure },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The message body height is computed from the actual text without touching the DOM. When a new token arrives, re-run the computation — it takes microseconds.
|
|
86
|
+
|
|
87
|
+
## Use Cases
|
|
88
|
+
|
|
89
|
+
- **AI streaming UIs** — compute height before rendering, eliminate scroll jank
|
|
90
|
+
- **Virtual scrolling** — accurate variable-height rows without measuring DOM elements
|
|
91
|
+
- **Canvas / WebGPU apps** — full UI layout in a draw loop
|
|
92
|
+
- **Server-side rendering** — layout computation in Node.js, Workers, Deno, Bun
|
|
93
|
+
- **PDF / document generation** — without headless Chrome
|
|
94
|
+
- **Layout unit testing** — without a browser
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
### `computeLayout(node, availableWidth?, availableHeight?)`
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface FlexNode {
|
|
102
|
+
style?: FlexStyle;
|
|
103
|
+
children?: FlexNode[];
|
|
104
|
+
measure?: (availableWidth: number, availableHeight: number) => { width: number; height: number };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface LayoutResult {
|
|
108
|
+
x: number;
|
|
109
|
+
y: number;
|
|
110
|
+
width: number;
|
|
111
|
+
height: number;
|
|
112
|
+
children: LayoutResult[];
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Supported Style Properties
|
|
117
|
+
|
|
118
|
+
**Container:**
|
|
119
|
+
- `display`: `'flex'` | `'grid'` | `'none'`
|
|
120
|
+
- `flexDirection`: `'row'` | `'column'` | `'row-reverse'` | `'column-reverse'`
|
|
121
|
+
- `flexWrap`: `'nowrap'` | `'wrap'` | `'wrap-reverse'`
|
|
122
|
+
- `justifyContent`: `'flex-start'` | `'flex-end'` | `'center'` | `'space-between'` | `'space-around'` | `'space-evenly'`
|
|
123
|
+
- `alignItems`: `'flex-start'` | `'flex-end'` | `'center'` | `'stretch'`
|
|
124
|
+
- `alignContent`: `'flex-start'` | `'flex-end'` | `'center'` | `'stretch'` | `'space-between'` | `'space-around'`
|
|
125
|
+
- `gap`, `rowGap`, `columnGap`
|
|
126
|
+
|
|
127
|
+
**CSS Grid:**
|
|
128
|
+
- `gridTemplateColumns`, `gridTemplateRows` — explicit track sizes (number, `'Nfr'`, `'auto'`)
|
|
129
|
+
- `gridAutoRows`, `gridAutoColumns` — implicit track sizes
|
|
130
|
+
- `gridColumnStart`, `gridColumnEnd`, `gridRowStart`, `gridRowEnd` — item placement
|
|
131
|
+
- `gridAutoFlow`: `'row'` | `'column'`
|
|
132
|
+
|
|
133
|
+
**Item:**
|
|
134
|
+
- `flexGrow`, `flexShrink`, `flexBasis`
|
|
135
|
+
- `alignSelf`: `'auto'` | `'flex-start'` | `'flex-end'` | `'center'` | `'stretch'`
|
|
136
|
+
|
|
137
|
+
**Sizing:**
|
|
138
|
+
- `width`, `height`: number (px), `'${n}%'`, or `'auto'`
|
|
139
|
+
- `minWidth`, `minHeight`, `maxWidth`, `maxHeight`
|
|
140
|
+
- `padding`, `paddingTop/Right/Bottom/Left`
|
|
141
|
+
- `margin`, `marginTop/Right/Bottom/Left` (including `'auto'`)
|
|
142
|
+
- `border`, `borderTop/Right/Bottom/Left`
|
|
143
|
+
- `boxSizing`: `'content-box'` | `'border-box'`
|
|
144
|
+
- `aspectRatio`: number
|
|
145
|
+
|
|
146
|
+
**Positioning:**
|
|
147
|
+
- `position`: `'relative'` | `'absolute'` | `'fixed'`
|
|
148
|
+
- `top`, `right`, `bottom`, `left`
|
|
149
|
+
|
|
150
|
+
### Measure Function
|
|
151
|
+
|
|
152
|
+
For leaf nodes with dynamic content (text, images), provide a `measure` callback:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const node = {
|
|
156
|
+
measure: (availableWidth, availableHeight) => ({
|
|
157
|
+
width: measureTextWidth(myText, availableWidth),
|
|
158
|
+
height: measureTextHeight(myText, availableWidth),
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This is how you integrate with text measurement libraries like [Pretext](https://pretext.dev).
|
|
164
|
+
|
|
165
|
+
## Performance
|
|
166
|
+
|
|
167
|
+
Benchmarked on Apple M-series, Node.js v24:
|
|
168
|
+
|
|
169
|
+
| Scenario | Items | Time | Ops/sec |
|
|
170
|
+
|----------|-------|------|---------|
|
|
171
|
+
| Flat row | 10 | 3.4 us | 292,000 |
|
|
172
|
+
| Flat row | 100 | 26 us | 38,000 |
|
|
173
|
+
| Flat row | 1,000 | 276 us | 3,600 |
|
|
174
|
+
| Flat row | 10,000 | 6.6 ms | 150 |
|
|
175
|
+
| Wrapped | 100 | 27 us | 36,500 |
|
|
176
|
+
| Wrapped | 1,000 | 280 us | 3,500 |
|
|
177
|
+
| Nested (84 nodes) | 84 | 204 us | 4,900 |
|
|
178
|
+
| Nested (780 nodes) | 780 | 5.2 ms | 192 |
|
|
179
|
+
|
|
180
|
+
## Comparison
|
|
181
|
+
|
|
182
|
+
| | Noreflow | Yoga | Textura |
|
|
183
|
+
|---|---|---|---|
|
|
184
|
+
| Architecture | Purpose-built TS engine | C++ -> WASM | Wraps Yoga |
|
|
185
|
+
| Language | Pure TypeScript | WASM binary | WASM binary |
|
|
186
|
+
| API | Pure function, data in/out | Class-based, manual `.free()` | Imperative |
|
|
187
|
+
| Initialization | Synchronous | `await init()` | `await init()` |
|
|
188
|
+
| CSS Grid | Yes | No | No |
|
|
189
|
+
| Aspect Ratio | Yes | Yes | Yes |
|
|
190
|
+
| Absolute/Fixed | Yes | Yes | Yes |
|
|
191
|
+
| Bundle | ~10 KB gzip | ~45 KB (WASM) | ~45 KB+ (WASM) |
|
|
192
|
+
| Dependencies | Zero | None | yoga-layout + pretext |
|
|
193
|
+
| Debugging | JS debugger | WASM boundary | WASM boundary |
|
|
194
|
+
| Tree-shakeable | Yes | No | No |
|
|
195
|
+
|
|
196
|
+
## Current Limitations
|
|
197
|
+
|
|
198
|
+
- Writing modes / RTL
|
|
199
|
+
- Baseline alignment
|
|
200
|
+
- Intrinsic sizing keywords (`min-content`, `max-content`, `fit-content`)
|
|
201
|
+
- `visibility: collapse`
|
|
202
|
+
- `position: sticky`
|
|
203
|
+
|
|
204
|
+
These are planned for future releases.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pnpm install
|
|
210
|
+
pnpm test # Run tests
|
|
211
|
+
pnpm test:watch # Watch mode
|
|
212
|
+
pnpm bench # Run benchmarks
|
|
213
|
+
pnpm build # Build ESM + CJS + types
|
|
214
|
+
pnpm typecheck # Type-check
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|