hadars 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A minimal server-side rendering framework for React built on [rspack](https://rspack.dev). Runs on Bun, Node.js, and Deno.
4
4
 
5
+ **[hadars.xyz](https://hadars.xyz)** — docs & website
6
+
5
7
  ## Why hadars?
6
8
 
7
9
  hadars is an alternative to Next.js for apps that just need SSR.
@@ -19,30 +21,30 @@ Bring your own router (or none), keep your components as plain React, and get SS
19
21
  ## Benchmarks
20
22
 
21
23
  <!-- BENCHMARK_START -->
22
- > Last run: 2026-03-23 · 120s · 100 connections · Bun runtime
23
- > hadars is **8.7x faster** in requests/sec
24
+ > Last run: 2026-03-25 · 120s · 100 connections · Bun runtime
25
+ > hadars is **8.9x faster** in requests/sec
24
26
 
25
27
  **Throughput** (autocannon, 120s)
26
28
 
27
29
  | Metric | hadars | Next.js |
28
30
  |---|---:|---:|
29
- | Requests/sec | **165** | 19 |
30
- | Latency median | **599 ms** | 2621 ms |
31
- | Latency p99 | **982 ms** | 3716 ms |
32
- | Throughput | **46.99** MB/s | 10.37 MB/s |
33
- | Peak RSS | 1038.2 MB | **489.6 MB** |
34
- | Avg RSS | 793.4 MB | **432.4 MB** |
35
- | Build time | 0.8 s | 6.0 s |
31
+ | Requests/sec | **151** | 17 |
32
+ | Latency median | **642 ms** | 2747 ms |
33
+ | Latency p99 | **959 ms** | 4019 ms |
34
+ | Throughput | **43.05** MB/s | 9.5 MB/s |
35
+ | Peak RSS | 950.3 MB | **478.5 MB** |
36
+ | Avg RSS | 763.3 MB | **426.4 MB** |
37
+ | Build time | 0.7 s | 6.0 s |
36
38
 
37
39
  **Page load** (Playwright · Chromium headless · median)
38
40
 
39
41
  | Metric | hadars | Next.js |
40
42
  |---|---:|---:|
41
- | TTFB | **18 ms** | 38 ms |
42
- | FCP | **96 ms** | 128 ms |
43
- | DOMContentLoaded | **39 ms** | 118 ms |
44
- | Load | **122 ms** | 164 ms |
45
- | Peak RSS | 496.7 MB | **304.8 MB** |
43
+ | TTFB | **19 ms** | 42 ms |
44
+ | FCP | **96 ms** | 136 ms |
45
+ | DOMContentLoaded | **39 ms** | 127 ms |
46
+ | Load | **122 ms** | 173 ms |
47
+ | Peak RSS | 476.8 MB | **289.5 MB** |
46
48
  <!-- BENCHMARK_END -->
47
49
 
48
50
  ## Quick start
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  renderPreflight,
3
3
  renderToString
4
- } from "./chunk-LY5MTHFV.js";
4
+ } from "./chunk-TV37IMRB.js";
5
5
  import {
6
6
  createElement
7
7
  } from "./chunk-OS3V4CPN.js";
@@ -230,18 +230,18 @@ var makePrecontentHtmlGetter = (htmlFilePromise) => {
230
230
  let preHead = null;
231
231
  let postHead = null;
232
232
  let postContent = null;
233
+ const primed = htmlFilePromise.then((html) => {
234
+ const headEnd = html.indexOf(HEAD_MARKER);
235
+ const contentStart = html.indexOf(BODY_MARKER);
236
+ preHead = html.slice(0, headEnd);
237
+ postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
238
+ postContent = html.slice(contentStart + BODY_MARKER.length);
239
+ });
233
240
  return (headHtml) => {
234
241
  if (preHead !== null) {
235
242
  return [preHead + headHtml + postHead, postContent];
236
243
  }
237
- return htmlFilePromise.then((html) => {
238
- const headEnd = html.indexOf(HEAD_MARKER);
239
- const contentStart = html.indexOf(BODY_MARKER);
240
- preHead = html.slice(0, headEnd);
241
- postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
242
- postContent = html.slice(contentStart + BODY_MARKER.length);
243
- return [preHead + headHtml + postHead, postContent];
244
- });
244
+ return primed.then(() => [preHead + headHtml + postHead, postContent]);
245
245
  };
246
246
  };
247
247
  async function transformStream(data, stream) {
@@ -264,8 +264,18 @@ async function transformStream(data, stream) {
264
264
  }
265
265
  return out;
266
266
  }
267
- var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
268
- var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
267
+ async function zlibGzip(d) {
268
+ const zlib = await import("node:zlib");
269
+ const { promisify } = await import("node:util");
270
+ return promisify(zlib.gzip)(d);
271
+ }
272
+ async function zlibGunzip(d) {
273
+ const zlib = await import("node:zlib");
274
+ const { promisify } = await import("node:util");
275
+ return promisify(zlib.gunzip)(d);
276
+ }
277
+ var gzipCompress = (d) => globalThis.CompressionStream ? transformStream(d, new globalThis.CompressionStream("gzip")) : zlibGzip(d);
278
+ var gzipDecompress = (d) => globalThis.DecompressionStream ? transformStream(d, new globalThis.DecompressionStream("gzip")) : zlibGunzip(d);
269
279
  async function buildCacheEntry(res, ttl) {
270
280
  const buf = await res.arrayBuffer();
271
281
  const body = await gzipCompress(new Uint8Array(buf));
@@ -115,7 +115,16 @@ function componentCalledUseId() {
115
115
  function snapshotContext() {
116
116
  const st = s();
117
117
  const ctx = st.currentTreeContext;
118
- return { tree: { id: ctx.id, overflow: ctx.overflow }, localId: st.localIdCounter, treeDepth: _treeDepth };
118
+ const depth = _treeDepth;
119
+ return {
120
+ tree: { id: ctx.id, overflow: ctx.overflow },
121
+ localId: st.localIdCounter,
122
+ treeDepth: depth,
123
+ // Snapshot the live stack so that popTreeContext reads correct saved values
124
+ // even if another concurrent render's resetRenderState stomped the arrays.
125
+ idStack: _treeIdStack.slice(0, depth),
126
+ ovStack: _treeOvStack.slice(0, depth)
127
+ };
119
128
  }
120
129
  function restoreContext(snap) {
121
130
  const st = s();
@@ -124,6 +133,10 @@ function restoreContext(snap) {
124
133
  ctx.overflow = snap.tree.overflow;
125
134
  st.localIdCounter = snap.localId;
126
135
  _treeDepth = snap.treeDepth;
136
+ for (let i = 0; i < snap.treeDepth; i++) {
137
+ _treeIdStack[i] = snap.idStack[i];
138
+ _treeOvStack[i] = snap.ovStack[i];
139
+ }
127
140
  }
128
141
  function getTreeId() {
129
142
  const { id, overflow } = s().currentTreeContext;
@@ -269,6 +282,14 @@ function restoreDispatcher(prev) {
269
282
  }
270
283
 
271
284
  // src/slim-react/render.ts
285
+ function captureRenderCtx() {
286
+ return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
287
+ }
288
+ function restoreRenderCtx(ctx) {
289
+ swapContextMap(ctx.m);
290
+ restoreUnsuspend(ctx.u);
291
+ restoreContext(ctx.t);
292
+ }
272
293
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
273
294
  "area",
274
295
  "base",
@@ -712,11 +733,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
712
733
  if (e && typeof e.then === "function") {
713
734
  if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
714
735
  patchPromiseStatus(e);
715
- const m = captureMap();
716
- const u = captureUnsuspend();
736
+ const rctx = captureRenderCtx();
717
737
  return e.then(() => {
718
- swapContextMap(m);
719
- restoreUnsuspend(u);
738
+ restoreRenderCtx(rctx);
720
739
  return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
721
740
  });
722
741
  }
@@ -753,17 +772,14 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
753
772
  };
754
773
  const r2 = renderChildren(props.children, writer, isSvg);
755
774
  if (r2 && typeof r2.then === "function") {
756
- const m = captureMap();
757
- const u = captureUnsuspend();
775
+ const rctx = captureRenderCtx();
758
776
  return r2.then(
759
777
  () => {
760
- swapContextMap(m);
761
- restoreUnsuspend(u);
778
+ restoreRenderCtx(rctx);
762
779
  finish();
763
780
  },
764
781
  (e) => {
765
- swapContextMap(m);
766
- restoreUnsuspend(u);
782
+ restoreRenderCtx(rctx);
767
783
  finish();
768
784
  throw e;
769
785
  }
@@ -792,11 +808,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
792
808
  if (e && typeof e.then === "function") {
793
809
  if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
794
810
  patchPromiseStatus(e);
795
- const m = captureMap();
796
- const u = captureUnsuspend();
811
+ const rctx = captureRenderCtx();
797
812
  return e.then(() => {
798
- swapContextMap(m);
799
- restoreUnsuspend(u);
813
+ restoreRenderCtx(rctx);
800
814
  return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
801
815
  });
802
816
  }
@@ -808,30 +822,25 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
808
822
  savedIdTree = pushTreeContext(1, 0);
809
823
  }
810
824
  if (result instanceof Promise) {
811
- const m = captureMap();
812
- const u = captureUnsuspend();
825
+ const rctx = captureRenderCtx();
813
826
  return result.then((resolved) => {
814
- swapContextMap(m);
815
- restoreUnsuspend(u);
827
+ restoreRenderCtx(rctx);
816
828
  let asyncSavedIdTree;
817
829
  if (componentCalledUseId()) {
818
830
  asyncSavedIdTree = pushTreeContext(1, 0);
819
831
  }
820
832
  const r2 = renderNode(resolved, writer, isSvg);
821
833
  if (r2 && typeof r2.then === "function") {
822
- const m2 = captureMap();
823
- const u2 = captureUnsuspend();
834
+ const rctx2 = captureRenderCtx();
824
835
  return r2.then(
825
836
  () => {
826
- swapContextMap(m2);
827
- restoreUnsuspend(u2);
837
+ restoreRenderCtx(rctx2);
828
838
  if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
829
839
  popComponentScope(savedScope);
830
840
  if (isProvider) popContextValue(ctx, prevCtxValue);
831
841
  },
832
842
  (e) => {
833
- swapContextMap(m2);
834
- restoreUnsuspend(u2);
843
+ restoreRenderCtx(rctx2);
835
844
  if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
836
845
  popComponentScope(savedScope);
837
846
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -843,8 +852,7 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
843
852
  popComponentScope(savedScope);
844
853
  if (isProvider) popContextValue(ctx, prevCtxValue);
845
854
  }, (e) => {
846
- swapContextMap(m);
847
- restoreUnsuspend(u);
855
+ restoreRenderCtx(rctx);
848
856
  popComponentScope(savedScope);
849
857
  if (isProvider) popContextValue(ctx, prevCtxValue);
850
858
  throw e;
@@ -852,19 +860,16 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
852
860
  }
853
861
  const r = renderNode(result, writer, isSvg);
854
862
  if (r && typeof r.then === "function") {
855
- const m = captureMap();
856
- const u = captureUnsuspend();
863
+ const rctx = captureRenderCtx();
857
864
  return r.then(
858
865
  () => {
859
- swapContextMap(m);
860
- restoreUnsuspend(u);
866
+ restoreRenderCtx(rctx);
861
867
  if (savedIdTree !== void 0) popTreeContext(savedIdTree);
862
868
  popComponentScope(savedScope);
863
869
  if (isProvider) popContextValue(ctx, prevCtxValue);
864
870
  },
865
871
  (e) => {
866
- swapContextMap(m);
867
- restoreUnsuspend(u);
872
+ restoreRenderCtx(rctx);
868
873
  if (savedIdTree !== void 0) popTreeContext(savedIdTree);
869
874
  popComponentScope(savedScope);
870
875
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -889,11 +894,9 @@ function renderChildArrayFrom(children, startIndex, writer, isSvg) {
889
894
  const savedTree = pushTreeContext(totalChildren, i);
890
895
  const r = renderNode(child, writer, isSvg);
891
896
  if (r && typeof r.then === "function") {
892
- const m = captureMap();
893
- const u = captureUnsuspend();
897
+ const rctx = captureRenderCtx();
894
898
  return r.then(() => {
895
- swapContextMap(m);
896
- restoreUnsuspend(u);
899
+ restoreRenderCtx(rctx);
897
900
  popTreeContext(savedTree);
898
901
  return renderChildArrayFrom(children, i + 1, writer, isSvg);
899
902
  });
@@ -917,11 +920,9 @@ async function renderSuspense(props, writer, isSvg = false) {
917
920
  try {
918
921
  const r = renderNode(children, buffer, isSvg);
919
922
  if (r && typeof r.then === "function") {
920
- const m = captureMap();
921
- const u = captureUnsuspend();
923
+ const rctx = captureRenderCtx();
922
924
  await r;
923
- swapContextMap(m);
924
- restoreUnsuspend(u);
925
+ restoreRenderCtx(rctx);
925
926
  }
926
927
  writer.write("<!--$-->");
927
928
  buffer.flushTo(writer);
@@ -935,11 +936,9 @@ async function renderSuspense(props, writer, isSvg = false) {
935
936
  if (fallback) {
936
937
  const r = renderNode(fallback, writer, isSvg);
937
938
  if (r && typeof r.then === "function") {
938
- const m = captureMap();
939
- const u = captureUnsuspend();
939
+ const rctx = captureRenderCtx();
940
940
  await r;
941
- swapContextMap(m);
942
- restoreUnsuspend(u);
941
+ restoreRenderCtx(rctx);
943
942
  }
944
943
  }
945
944
  writer.write("<!--/$-->");
@@ -976,9 +975,9 @@ function renderToStream(element, options) {
976
975
  try {
977
976
  const r = renderNode(element, writer);
978
977
  if (r && typeof r.then === "function") {
979
- const m = captureMap();
978
+ const rctx = captureRenderCtx();
980
979
  await r;
981
- swapContextMap(m);
980
+ restoreRenderCtx(rctx);
982
981
  }
983
982
  writer.flush();
984
983
  controller.close();
@@ -1005,9 +1004,9 @@ async function renderPreflight(element, options) {
1005
1004
  NULL_WRITER.lastWasText = false;
1006
1005
  const r = renderNode(element, NULL_WRITER);
1007
1006
  if (r && typeof r.then === "function") {
1008
- const m = captureMap();
1007
+ const rctx = captureRenderCtx();
1009
1008
  await r;
1010
- swapContextMap(m);
1009
+ restoreRenderCtx(rctx);
1011
1010
  }
1012
1011
  } finally {
1013
1012
  swapContextMap(prev);
@@ -1032,9 +1031,9 @@ async function renderToString(element, options) {
1032
1031
  resetRenderState(idPrefix);
1033
1032
  const r = renderNode(element, writer);
1034
1033
  if (r && typeof r.then === "function") {
1035
- const m = captureMap();
1034
+ const rctx = captureRenderCtx();
1036
1035
  await r;
1037
- swapContextMap(m);
1036
+ restoreRenderCtx(rctx);
1038
1037
  }
1039
1038
  return output;
1040
1039
  } finally {