You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document captures a holistic pass over `rescript-relay-router` with a focus on runtime performance, bundle shape, code splitting, SSR asset loading, and maintainability of the hot paths.
3
+
This document captures a holistic pass over `rescript-relay-router` with a focus on runtime performance, bundle shape, code splitting, and maintainability of the hot paths.
4
4
5
5
The intent is that each section can be picked up as an independent work item. Some items are direct optimizations with low semantic risk. Others are architectural directions that should start with measurement or a narrow proof of concept.
| P0 | Centralize manifest-backed asset preloading | High for SSR, hydration, and route transitions | Medium | SSR/build integration |
12
11
| P1 | Reuse unchanged prepared matches with explicit freshness policy | High for nested routes with parent queries | High | Relay/runtime |
13
12
| P1 | Centralize location subscriptions | Medium to high in nav-heavy UIs | Medium | React runtime |
14
13
| P2 | Split route declaration payload from route prepare modules | High in very large apps | High | Codegen/build |
@@ -39,13 +38,12 @@ Before changing behavior, add a repeatable way to measure the current costs:
39
38
- Measure number of `prepare` calls per navigation.
40
39
- Measure number of history listeners on a page with many active route hooks.
41
40
- Measure client bundle size and chunk graph for a realistic app.
42
-
- Measure SSR output for preload tag duplication, preload ordering, and hydration timing.
43
41
44
42
Suggested test fixtures:
45
43
46
44
- Extend `packages/rescript-relay-router/test/RouterUtils.test.res` for pure route matching and route-key behavior.
47
45
- Add focused tests around generated output in `packages/rescript-relay-router/test/RescriptRelayRouterCli.test.res`.
48
-
- Add a larger example or benchmark fixture under `examples/` only if it exercises Vite chunk output or SSR manifest behavior.
46
+
- Add a larger example or benchmark fixture under `examples/` only if it exercises realistic route trees and Vite chunk output.
49
47
50
48
Use this baseline to prevent a common failure mode: making the router theoretically cleaner while moving cost from one phase to another without observing the final app behavior.
51
49
@@ -94,103 +92,7 @@ uses the compiled matcher for initial matching, history updates, and route prelo
94
92
95
93
- Future vendored React Router changes still need parity tests because this helper is modified.
96
94
97
-
## 2. Centralize Manifest-Backed Asset Preloading
98
-
99
-
### Current State
100
-
101
-
The router has a preload abstraction:
102
-
103
-
-`Component`
104
-
-`Image`
105
-
-`Style`
106
-
107
-
Client-side preloading currently de-dupes asset identifiers, but only component assets actually do anything:
Move the default manifest-aware preloading logic into the router package. Make examples consume that default instead of implementing their own.
131
-
132
-
The router should provide two default preload implementations:
133
-
134
-
- Client: de-duped `import()` for component chunks, DOM `<link>` insertion for CSS/images/modules when useful.
135
-
- Server: de-duped HTML tag emission through `PreloadInsertingStream`.
136
-
137
-
The core design should make asset preloading explicit and safe:
138
-
139
-
- Component chunks should generally use `rel="modulepreload"` on the server, not an eager script execution tag, unless there is a deliberate reason to execute.
140
-
- CSS should use `rel="stylesheet"` or `rel="preload" as="style"` with clear tradeoffs.
141
-
- Images should use `rel="preload" as="image"` only when the asset type is known.
142
-
- Unknown assets should either not be preloaded or should carry metadata from the manifest transform.
143
-
144
-
### Implementation Steps
145
-
146
-
1. Extend `RelayRouter__Manifest.file` with enough metadata to avoid guessing asset types.
147
-
- Option A: store assets as records with `url` and `kind`.
148
-
- Option B: split manifest fields into `images`, `fonts`, `assets`, etc.
149
-
- Option C: keep raw assets but infer from extension in one central helper.
150
-
2. Add a shared `AssetPreloader` helper that can expand a component chunk into:
151
-
- the chunk itself
152
-
- recursive static imports
153
-
- associated CSS
154
-
- associated known assets
155
-
3. Ensure recursion is cycle-safe and de-duped by URL.
156
-
4. Add a server preloader constructor, for example:
157
-
158
-
```rescript
159
-
let makeServerAssetPreloader: (
160
-
~manifest: RelayRouter__Manifest.t,
161
-
~emit: string => unit,
162
-
) => RelayRouter__Types.preloadAssetFn
163
-
```
164
-
165
-
5. Add a client preloader constructor that optionally accepts the manifest:
166
-
167
-
```rescript
168
-
let makeClientAssetPreloader: (
169
-
preparedAssetsMap,
170
-
~manifest: option<RelayRouter__Manifest.t>=?,
171
-
) => RelayRouter__Types.preloadAssetFn
172
-
```
173
-
174
-
6. Replace the custom SSR implementation in `examples/express/src/EntryServer.res` with the router-provided default.
175
-
7. Preserve the current user extension point: callers can still pass a custom `preloadAsset`.
176
-
8. Add tests for de-dupe and manifest expansion.
177
-
178
-
### Validation
179
-
180
-
- SSR output includes no duplicate preload tags for the same URL.
181
-
- Component routes emit module preload tags for the exact chunks needed.
182
-
- CSS needed by matched chunks is emitted.
183
-
- Unknown asset types are not mislabeled as images.
184
-
- Existing examples still hydrate.
185
-
- Client-side `Image` and `Style` preloads are no longer no-ops.
186
-
187
-
### Risks
188
-
189
-
- Browser preload behavior is sensitive. `modulepreload`, `preload`, and actual script execution are not interchangeable.
190
-
- Over-preloading can hurt performance. The implementation should respect priority and make aggressive behavior opt-in where possible.
191
-
- Vite manifest shape can differ across versions and config options.
192
-
193
-
## 3. Reuse Unchanged Prepared Matches With an Explicit Freshness Policy
95
+
## 2. Reuse Unchanged Prepared Matches With an Explicit Freshness Policy
194
96
195
97
### Current State
196
98
@@ -258,7 +160,7 @@ Defaulting this is the important design decision:
258
160
- The policy needs clear docs. Users should understand when reuse is safe.
259
161
- A custom policy can become an API surface that is hard to evolve.
260
162
261
-
## 4. Split the Route Declaration Payload From Lazy Route Prepare Modules
163
+
## 3. Split the Route Declaration Payload From Lazy Route Prepare Modules
262
164
263
165
### Current State
264
166
@@ -284,7 +186,6 @@ Split the generated route output into:
284
186
1. A small always-loaded match manifest:
285
187
- route path
286
188
- route name
287
-
- route chunk identifier
288
189
- children
289
190
2. Lazy per-route modules:
290
191
- prepare prop decoding
@@ -298,7 +199,7 @@ The runtime matcher only needs the match manifest. The router only needs the laz
298
199
299
200
1. Define a smaller route matching type, separate from the executable `route` type.
300
201
2. Update the matcher binding to operate on this smaller type if needed.
301
-
3. Generate a manifest-only `RouteDeclarations.makeManifest()`.
202
+
3. Generate a match-only `RouteDeclarations.makeRouteTree()`.
302
203
4. Generate per-route loader modules that can produce the executable route behavior when matched.
303
204
5. Update `Router.make` to accept either:
304
205
- current eager route objects
@@ -311,15 +212,15 @@ The runtime matcher only needs the match manifest. The router only needs the laz
311
212
- Initial client bundle shrinks for a large generated route tree.
312
213
- Route matching still works without loading unmatched route prepare modules.
313
214
- Preloading a link loads only the modules for the matched branch.
314
-
-SSR still has enough manifest data to emit correct preload tags.
215
+
-The server can still construct the match tree without browser APIs.
315
216
316
217
### Risks
317
218
318
219
- This is a larger architectural shift.
319
220
- It may complicate type-safe generated route module access.
320
221
- Dynamic import boundaries from generated code need careful Vite/Rollup testing.
321
222
322
-
## 5. Verify and Improve Renderer Chunk Boundaries
223
+
## 4. Verify and Improve Renderer Chunk Boundaries
323
224
324
225
### Current State
325
226
@@ -367,7 +268,7 @@ This may produce cleaner and smaller async chunks.
367
268
- ReScript or Vite may already tree-shake this well enough, making the change unnecessary.
368
269
- The `Routes` access path is more ergonomic. Direct module references are less discoverable.
369
270
370
-
## 6. Centralize Location Subscriptions
271
+
## 5. Centralize Location Subscriptions
371
272
372
273
### Current State
373
274
@@ -419,7 +320,7 @@ The key goal is one history listener per router instance, not one per hook.
419
320
- Current router route-entry updates skip shallow navigations. Location subscribers must not accidentally inherit that behavior if query param hooks should update.
420
321
- React transition behavior should be reviewed. Some location updates may want transition scheduling, others may not.
421
322
422
-
## 7. Make Route Keys Collision-Safe
323
+
## 6. Make Route Keys Collision-Safe
423
324
424
325
### Landed Shape
425
326
@@ -450,7 +351,7 @@ query param arrays. It distinguishes:
450
351
451
352
- If current accidental collisions masked bugs, this can expose them.
452
353
453
-
## 8. Route Declaration Entrypoints
354
+
## 7. Route Declaration Entrypoints
454
355
455
356
### Use Case
456
357
@@ -565,7 +466,7 @@ This behavior is preferable to render-time filtering because it scopes matching,
565
466
- Generating per-root files can complicate the current generated module structure and LSP helpers.
566
467
- If top-level modules share `loadedRouteRenderers`, code loading can be shared across contexts. If they do not, duplicate dynamic import state may be tracked. Decide explicitly.
567
468
568
-
## 9. Pool Link Intersection Observers
469
+
## 8. Pool Link Intersection Observers
569
470
570
471
### Current State
571
472
@@ -618,7 +519,7 @@ The default should be conservative, but the current threshold may be too late to
618
519
- Root element identity is awkward as a dictionary key. A JS helper may be cleaner than doing all registry logic in ReScript.
619
520
- Early preloading can increase network usage. Defaults should be measured.
620
521
621
-
## 10. Replace Reflective Disposable Extraction
522
+
## 9. Replace Reflective Disposable Extraction
622
523
623
524
### Current State
624
525
@@ -678,7 +579,7 @@ The second option avoids wrapping existing prepare returns but still gives expli
678
579
- The API may become noisier for users.
679
580
- Having both explicit and reflective behavior can be confusing. Docs should state precedence clearly.
680
581
681
-
## 11. Reduce Repeated Query Decoding
582
+
## 10. Reduce Repeated Query Decoding
682
583
683
584
### Current State
684
585
@@ -730,48 +631,16 @@ The simpler near-term improvement is to reuse parsed `QueryParams.t` across rout
730
631
- The cache may need to return fresh copies for mutation-heavy call sites.
731
632
- Over-caching can introduce subtle stale-state bugs.
732
633
733
-
## 12. SSR Stream Logging and Cleanup
734
-
735
-
### Current State
736
-
737
-
`RelaySSRUtils` logs a lot of debug information directly:
This includes streamed entry events, boot events, and replay subject cleanup. It is not the largest performance issue, but it can add noise and overhead in production SSR/hydration flows.
742
-
743
-
### Opportunity
744
-
745
-
Gate debug logs behind a configuration flag or remove them from the default path.
746
-
747
-
This belongs in the same family as SSR asset preloading because both affect response streaming and hydration behavior.
748
-
749
-
### Implementation Steps
750
-
751
-
1. Add a debug flag or logging callback.
752
-
2. Default it to disabled.
753
-
3. Replace direct `Console.log` calls with the gated logger.
754
-
4. Add a test or example check that production usage does not log by default.
755
-
756
-
### Validation
757
-
758
-
- No debug logs in normal example app boot.
759
-
- Logs can still be enabled for stream debugging.
760
-
761
-
### Risks
762
-
763
-
- Debugging SSR streaming is hard. Keep an easy way to re-enable logs.
764
-
765
634
## Suggested Work Slicing
766
635
767
636
These work items can be picked up independently:
768
637
769
-
1. Server/client asset preloader:
770
-
- practical SSR and code-splitting payoff
771
-
- likely needs manifest type changes
772
-
2. Location subscription store:
638
+
1. Location subscription store:
773
639
- contained React runtime change
774
640
- needs care around shallow navigation
641
+
2. Named root-tree rendering:
642
+
- codegen and runtime scoping change
643
+
- useful for multi-entry apps, admin surfaces, and embedded route renderers
775
644
3. Link observer pooling:
776
645
- contained link component change
777
646
- needs browser behavior validation
@@ -789,20 +658,18 @@ These work items can be picked up independently:
789
658
- may become a small scaffold generation change
790
659
8. Query decoding cache:
791
660
- useful after route-key and location-store work clarify ownership of `search`
792
-
9. SSR stream logging cleanup:
793
-
- small server-runtime cleanup
794
-
- should preserve opt-in debugging
795
661
796
662
## Recommended Starting Order
797
663
798
664
Start with:
799
665
800
-
1.Centralized asset preloading.
666
+
1.Named root-tree rendering, if multi-entry apps or embedded surfaces are a near-term target.
801
667
2. Centralized location subscriptions.
802
668
3. Reduce repeated query decoding.
803
669
4. Prepare freshness policy, behind an explicit opt-in.
804
670
5. Renderer chunk boundary verification before any larger lazy route declaration split.
805
671
806
-
That order focuses on the remaining SSR/code-splitting payoff first, then reduces router subscription
807
-
and query parsing churn before touching Relay freshness semantics. The larger lazy declaration split
808
-
should wait for bundle analysis so the architectural work is justified by measured chunk output.
672
+
That order improves route-tree boundaries for multi-entry rendering, then reduces router
673
+
subscription and query parsing churn before touching Relay freshness semantics. The larger lazy
674
+
declaration split should wait for bundle analysis so the architectural work is justified by
0 commit comments