@luigisbox/luma 0.0.0-stage → 0.2.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.
Files changed (145) hide show
  1. package/LICENSE.md +90 -0
  2. package/README.md +615 -2
  3. package/a11y/audit.mjs +735 -0
  4. package/a11y/axe.mjs +73 -0
  5. package/a11y/report.mjs +347 -0
  6. package/a11y/run.mjs +256 -0
  7. package/a11y/wcag.mjs +233 -0
  8. package/bin/config.mjs +60 -0
  9. package/bin/luma.mjs +99 -0
  10. package/bin/paths.mjs +11 -0
  11. package/bin/tools.mjs +43 -0
  12. package/build/baseline.d.mts +8 -0
  13. package/build/baseline.mjs +167 -0
  14. package/build/tooling.mjs +102 -0
  15. package/build/vite.mjs +507 -0
  16. package/chrome/block.mjs +8 -0
  17. package/chrome/run.mjs +150 -0
  18. package/docs/ACCESSIBILITY.md +501 -0
  19. package/docs/INTEGRATION-PERFORMANCE.md +213 -0
  20. package/docs/PARITY.md +946 -0
  21. package/footprint/model.mjs +184 -0
  22. package/footprint/run.mjs +331 -0
  23. package/package.json +58 -4
  24. package/parity/alignment.mjs +109 -0
  25. package/parity/archive.mjs +223 -0
  26. package/parity/args.mjs +137 -0
  27. package/parity/behaviour.mjs +379 -0
  28. package/parity/compare.mjs +985 -0
  29. package/parity/engine.mjs +848 -0
  30. package/parity/hints.mjs +132 -0
  31. package/parity/hold.mjs +54 -0
  32. package/parity/init.mjs +703 -0
  33. package/parity/map.mjs +640 -0
  34. package/parity/owner.mjs +170 -0
  35. package/parity/pairing.mjs +110 -0
  36. package/parity/pixels.mjs +33 -0
  37. package/parity/probe.mjs +1239 -0
  38. package/parity/report.mjs +236 -0
  39. package/parity/run.mjs +267 -0
  40. package/snippet/early-hold.js +64 -0
  41. package/snippet/early-hold.min.js +1 -0
  42. package/src/analytics/chain.ts +137 -0
  43. package/src/analytics/collect.ts +64 -0
  44. package/src/analytics/datalayer.ts +67 -0
  45. package/src/analytics/from-ga4.ts +68 -0
  46. package/src/analytics/from-luma.ts +21 -0
  47. package/src/analytics/index.ts +24 -0
  48. package/src/analytics/outbox.ts +118 -0
  49. package/src/analytics/page.ts +46 -0
  50. package/src/analytics/pageview.ts +82 -0
  51. package/src/analytics/ref.ts +19 -0
  52. package/src/analytics/setup.ts +41 -0
  53. package/src/analytics/start.ts +53 -0
  54. package/src/analytics/triggers.ts +239 -0
  55. package/src/analytics/user.ts +12 -0
  56. package/src/analytics/wire.ts +67 -0
  57. package/src/api/ai.ts +98 -0
  58. package/src/api/index.ts +87 -0
  59. package/src/api/token-key.ts +42 -0
  60. package/src/api/token.ts +312 -0
  61. package/src/core/a11y-keys.ts +60 -0
  62. package/src/core/a11y.ts +160 -0
  63. package/src/core/boot-state.ts +6 -0
  64. package/src/core/color.ts +42 -0
  65. package/src/core/context.ts +108 -0
  66. package/src/core/early-rules.ts +96 -0
  67. package/src/core/entries.ts +25 -0
  68. package/src/core/fold.ts +7 -0
  69. package/src/core/health.ts +26 -0
  70. package/src/core/hold.ts +74 -0
  71. package/src/core/i18n.ts +66 -0
  72. package/src/core/image-load.ts +102 -0
  73. package/src/core/image-tags.ts +26 -0
  74. package/src/core/images.ts +170 -0
  75. package/src/core/integration.ts +126 -0
  76. package/src/core/kept.ts +25 -0
  77. package/src/core/listings.ts +100 -0
  78. package/src/core/locales.ts +14 -0
  79. package/src/core/own.ts +49 -0
  80. package/src/core/page.ts +14 -0
  81. package/src/core/prefetch.ts +88 -0
  82. package/src/core/recommenders.ts +166 -0
  83. package/src/core/reconciler.ts +397 -0
  84. package/src/core/result.ts +73 -0
  85. package/src/core/routes.ts +115 -0
  86. package/src/core/scheduler.ts +83 -0
  87. package/src/core/sense.ts +107 -0
  88. package/src/core/speculation.ts +59 -0
  89. package/src/core/stash.ts +38 -0
  90. package/src/core/storage.ts +45 -0
  91. package/src/core/styles.ts +106 -0
  92. package/src/core/support.ts +12 -0
  93. package/src/core/takeover.ts +220 -0
  94. package/src/core/templates.ts +74 -0
  95. package/src/core/tiles.ts +137 -0
  96. package/src/core/turbo.ts +64 -0
  97. package/src/core/url.ts +164 -0
  98. package/src/core/widget-inits.ts +40 -0
  99. package/src/env.d.ts +3 -0
  100. package/src/lit.ts +11 -0
  101. package/src/locales/cs.json +94 -0
  102. package/src/locales/de.json +88 -0
  103. package/src/locales/en.json +88 -0
  104. package/src/main.ts +493 -0
  105. package/src/types.ts +778 -0
  106. package/src/widgets/autocomplete/index.ts +1387 -0
  107. package/src/widgets/autocomplete/style-autocomplete.css +266 -0
  108. package/src/widgets/autocomplete/template-autocomplete-box.ts +61 -0
  109. package/src/widgets/autocomplete/template-autocomplete-empty.ts +53 -0
  110. package/src/widgets/autocomplete/template-autocomplete-result-default.ts +82 -0
  111. package/src/widgets/autocomplete/template-autocomplete-sheet.ts +74 -0
  112. package/src/widgets/listing/element.ts +1772 -0
  113. package/src/widgets/listing/flow.ts +30 -0
  114. package/src/widgets/listing/instant.ts +133 -0
  115. package/src/widgets/listing/intent.ts +193 -0
  116. package/src/widgets/listing/query.ts +298 -0
  117. package/src/widgets/listing/stash.ts +97 -0
  118. package/src/widgets/listing/state.ts +131 -0
  119. package/src/widgets/listing/view.ts +257 -0
  120. package/src/widgets/plp/index.ts +11 -0
  121. package/src/widgets/plp/route.ts +263 -0
  122. package/src/widgets/recommender/index.ts +261 -0
  123. package/src/widgets/recommender/style-recommender.css +26 -0
  124. package/src/widgets/recommender/template-recommender-box-default.ts +11 -0
  125. package/src/widgets/recommender/template-recommender-popup.ts +12 -0
  126. package/src/widgets/recommender/template-recommender-result-default.ts +10 -0
  127. package/src/widgets/search/assets.ts +7 -0
  128. package/src/widgets/search/first.ts +15 -0
  129. package/src/widgets/search/form-url.ts +59 -0
  130. package/src/widgets/search/in-place.ts +226 -0
  131. package/src/widgets/search/index.ts +16 -0
  132. package/src/widgets/search/range.ts +260 -0
  133. package/src/widgets/search/route.ts +32 -0
  134. package/src/widgets/search/speculate.ts +202 -0
  135. package/src/widgets/search/style-search.css +65 -0
  136. package/src/widgets/search/template-search-box.ts +17 -0
  137. package/src/widgets/search/template-search-facets-default.ts +73 -0
  138. package/src/widgets/search/template-search-header.ts +14 -0
  139. package/src/widgets/search/template-search-pagination.ts +12 -0
  140. package/src/widgets/search/template-search-range-default.ts +48 -0
  141. package/src/widgets/search/template-search-result-default.ts +13 -0
  142. package/src/widgets/search/template-search-results-default.ts +8 -0
  143. package/tasks/model.mjs +116 -0
  144. package/tasks/run.mjs +305 -0
  145. package/tsconfig.json +14 -0
@@ -0,0 +1,213 @@
1
+ # Integration performance: what to measure, what to optimize
2
+
3
+ A guideline for integration work (an integration repository with Luma as the
4
+ `@luigisbox/luma` package). Luma's own budgets are
5
+ held by its own test suites. An integration can still undo them with heavy tiles, a
6
+ layout that moves, or a slow install. This page lists the numbers to watch,
7
+ in priority order, and the levers an integration has.
8
+
9
+ ## The metrics, in priority order
10
+
11
+ | # | Metric | Target | Where to read it |
12
+ |---|---|---|---|
13
+ | 1 | **Time to listing shown** (first tiles painted, from navigation) | Repeat visit ≤ 1.2 s; first visit as low as the API chain allows (below) | `__lbx.health.timeline`, `yarn luma footprint` (`ready`), DevTools Performance |
14
+ | 2 | **Layout shift caused by us** | 0 after ours is shown | `yarn luma footprint`, a `layout-shift` PerformanceObserver, DevTools Performance (Layout shifts track) |
15
+ | 3 | **Longest main-thread task of ours** | ≤ 16 ms of our own code (a frame) at 4x CPU | `yarn luma tasks` (per task, on the storefront, by source line); the `rendered in N frames (a/b/c ms)` mark; DevTools at 4x |
16
+ | 4 | **Script chain before the listing renders** | Entry ≤ 14 kB brotli (1 round trip); chain depth ≤ 3 | `yarn luma footprint` (entry size, round trips, chain) |
17
+ | 5 | **API requests per page** | Category: lookup (first visit) + products + facets. Search: products + facets + one per quick type. Token: once per session | `yarn luma footprint` (API requests), DevTools Network |
18
+ | 6 | **Recommenders** | Inline: never empty in view. Popup: open within ~100 ms of the notice | DevTools; `reserve` set |
19
+ | 7 | **Bytes per scenario** | No growth without a reason | `yarn luma footprint` (scripts and bytes per page); `--dictionary` for a returning visitor after a deploy |
20
+ | 8 | **Autocomplete as you type** | Keystroke to next paint ≤ 50 ms (INP), first opening included; the typed phrase shown ≤ debounce + one answer after the last key | Event Timing (`event` entries), `ac ask <q>` / `ac shown <q>` in `__lbx.health.timeline`, `yarn luma footprint` (typing) |
21
+
22
+ What does not matter much: total JS bytes of chunks that load after the
23
+ listing shows (autocomplete, recommender), CORS preflights (cached 20 min),
24
+ and Lighthouse scores on pages without a listing.
25
+
26
+ ### 1. Time to listing shown
27
+
28
+ This is the number shoppers feel. It is a chain:
29
+
30
+ ```
31
+ HTML → loader → entry (1 RTT) → [challenge → token: first page of a session]
32
+ → [category lookup: first visit of that category] → products → (facets)
33
+ → render (first screen in ~100–120 ms at 4x) → shown
34
+ ```
35
+
36
+ - **On a real network, not in a sandbox.** Behind a proxy, the connect and TLS
37
+ setup alone can take several hundred ms per host, which hides what
38
+ preconnects save. Measure on your machine with `yarn luma footprint --rtt 100` (an
39
+ emulated 100 ms round trip), and with DevTools on the live site.
40
+ - Read it in the page:
41
+
42
+ ```js
43
+ __lbx.health.timeline // [ms since navigation, what]; for example (illustrative):
44
+ // [ 812, 'plp render 24 hits' ]
45
+ // [ 941, 'plp rendered in 4 frames (9/14/15/12 ms)' ]
46
+ // [ 760, 'plp products before their facets' ]
47
+ // [1210, 'search quick types after the products' ]
48
+ ```
49
+
50
+ Together with the Network panel: the gap between the products response
51
+ and `render` is ours. The gap between navigation and the products request is
52
+ the chain above.
53
+ - Reference (a production integration on its stub page, 4x CPU): ours is shown 108–121 ms after its
54
+ render starts. The rest of the listing finishes 90–140 ms later, busy
55
+ meanwhile. A repeat visit shows in 0.9–1.2 s. A first visit is dominated by
56
+ the serial API chain (challenge, token, lookup, products).
57
+ - **What an integration controls:**
58
+ - the snippet in `<head>` (below);
59
+ - where the facets and quick types sit. Beside the grid, in a drawer, or
60
+ below it, the products are shown without waiting for them. Above the
61
+ grid, they wait so that they don't push it down. Luma learns this per
62
+ route and width; you choose it with the layout;
63
+ - tile cost: a tile template that renders in ≤ 1 ms at 4x;
64
+ - `size`: the first page is what is rendered; a screen plus a row is enough,
65
+ and "show more" is cheap.
66
+
67
+ ### 2. Layout shift
68
+
69
+ Zero after ours is shown. The first frame's swap (their listing collapsing as
70
+ ours takes its place) is the takeover's and expected. Anything after it is a
71
+ bug in the integration or in Luma. Sources, most common first:
72
+
73
+ - **Images without a box.** Use `image()` / `img()` from templates: they emit
74
+ `aspect-ratio` from `images.width/height`. Set those to the catalog's real
75
+ ratio.
76
+ - **Parts arriving late above the grid.** Luma waits for the facets and quick
77
+ types where they sit above, but a template that renders something else
78
+ above the list from data that comes later (a banner, a count) shifts it.
79
+ Use `facetsPending` / `quickPending` in the scope to keep their place.
80
+ - **Fonts.** Ours shows in the site's final font; a tile that uses a font the
81
+ page does not load swaps later. Use the site's fonts.
82
+ - **Recommenders** without `reserve`: the block grows when it fills. Set
83
+ `reserve` to the rendered height.
84
+ - **The reserved height of a hard load** (`reserveSelector`, the remembered
85
+ height): check with the footer in view on a tall screen.
86
+
87
+ Measure it with:
88
+
89
+ ```js
90
+ new PerformanceObserver((l) => l.getEntries().forEach((e) => !e.hadRecentInput && console.log(e.startTime|0, e.value, e.sources?.map((s) => s.node)))).observe({ type: 'layout-shift', buffered: true });
91
+ ```
92
+
93
+ ### 3. Main thread: no task of ours over a frame (16 ms) at 4x CPU
94
+
95
+ `yarn luma tasks` measures it on the storefront (or the stub page): every
96
+ scenario (the `luma.config.mjs` pages loaded, a change of the listing's state, show
97
+ more, Back, typing) three times at 4x CPU, and for each the worst task by
98
+ **our own** time, with its functions named by the integration's source. Our
99
+ own time leaves out the site's code that runs inside our calls (its
100
+ `pushState` hooks, its handlers an integration hook calls): that is in
101
+ `ours`, reported, but not ours to make faster. Exit 1 when a scenario's
102
+ median is over the budget (`--budget`, default 16).
103
+
104
+ What it found on a production integration, integration-side, the kind of
105
+ thing to look for:
106
+
107
+ - work at a module's top level in `_init.ts`: an `Intl.NumberFormat` made
108
+ there loads its locale's data on every page (18 ms of a 30 ms boot task);
109
+ make such things lazily, or not at all (a fixed price format written out);
110
+ - an `onOpen` that calls into the site (its search overlay opening, jQuery
111
+ `:visible` checks forcing a layout): 9-18 ms in the autocomplete's first
112
+ opening; Luma draws the rows in a task before it, but the hook's own cost
113
+ is the integration's;
114
+ - the browser's `history.pushState` (8-15 ms on a heavy page) in the URL
115
+ write after a filter: reported under `core/sense.ts`, not reducible by us.
116
+
117
+ The first render runs in steps: the first 4 tiles with the skeleton, then
118
+ the facet groups and quick types, then the rest, each step within ~16 ms. The
119
+ `rendered in N frames (…ms)` mark lists every step's time. A step over 30 ms
120
+ means the template is too heavy. Typical causes:
121
+
122
+ - per-tile work in the template (formatting, regexes, `toLocaleString` per
123
+ call): precompute it in `_init.ts` or in a `result` mapper;
124
+ - deep markup per tile (SVG icons inline per tile: use a `<use href>` sprite);
125
+ - many facet values rendered open (`facetSize`, collapsed groups).
126
+
127
+ `contentVisibility: true` (opt-in) makes re-renders after "show more" and
128
+ sorting cheaper on long listings (style, layout and paint 243 → 144 ms on
129
+ a production integration's listing), but it contains every tile: check tile hover effects, absolutely
130
+ positioned badges and overflowing shadows before turning it on.
131
+
132
+ ### 4. Scripts: size and chain
133
+
134
+ `yarn luma footprint` reports the entry (brotli) and the round trips it needs. Keep
135
+ the entry within the TCP first flight (14 kB): one round trip. Integration
136
+ code in `_init.ts` and `_config.ts` lands in the entry. Keep it small: no
137
+ libraries, no catalog data, no templates (templates go to their widget's
138
+ chunk). `--preload` shows what a loader's preload hint for the scenario's
139
+ chunks would save (−100 to −300 ms at 100 ms RTT on a production integration).
140
+ `--dictionary <previous dist>` shows what a returning visitor downloads after a
141
+ deploy with Use-As-Dictionary (category page 56.4 kB → 3.7 kB after a
142
+ one-commit change).
143
+
144
+ ### 5. API requests
145
+
146
+ Every request is a round trip on a first visit and load on the API.
147
+ Luma's own suites pin its counts. An integration adds
148
+ requests by:
149
+
150
+ - quick types (one request each; 2 is typical);
151
+ - `emptyValues` (the unfiltered facets, once per scope or query, on a filtered page);
152
+ - several recommenders on a page (each its own request);
153
+ - range facets over fields that are not numeric (a lookup per scope).
154
+
155
+ The token is minted once per session, off the main thread. The challenge is
156
+ asked by the entry while the minting code loads.
157
+
158
+ ### 6. Recommenders
159
+
160
+ Inline recommenders are asked two screens ahead, after the page's load event
161
+ (not competing with it). Popups are asked at the click that opens them.
162
+ Watch:
163
+
164
+ - **empty in view:** scroll at normal speed; the block must be filled
165
+ before it enters the viewport;
166
+ - `reserve`: the block's rendered height, so filling it does not shift.
167
+
168
+ ### 7. Autocomplete as you type
169
+
170
+ Two numbers. **Input latency**: no keystroke waits on us. The first opening is
171
+ the risky one: its one-time costs (templates, the first style and layout of
172
+ each text style) are rehearsed after focus. **Freshness**: how far the
173
+ dropdown trails the phrase. A batch goes out on the first 3 characters, then
174
+ after 300 ms of quiet or every 800 ms while typing (`instantChars`,
175
+ `debounceMs`, `maxWaitMs`). It renders once every type has answered, so the
176
+ slowest type sets the pace (usually products).
177
+
178
+ - The timeline pairs each phrase's `ac ask <q>` with its `ac shown <q>`. The
179
+ gap is the API's answer plus the pictures (≤ 100 ms) plus our render.
180
+ - Keep product rows light: few `fields`, `size` ≤ 7. Every field is server time
181
+ and bytes on every keystroke batch.
182
+ - Templates: the same rules as tiles (no per-row formatting work). A batch's
183
+ render should stay within one frame at 4x on a phone.
184
+ - Measure with real typing (150–250 ms between keys), cold (first opening on
185
+ a page) and warm. On a phone the sheet opens on the tap: that tap's task is
186
+ the one to watch.
187
+
188
+ ## How to measure, step by step
189
+
190
+ 0. `yarn luma tooling`: the integration has the one `luma` script and its
191
+ storefront's host in `luma.config.mjs` (`--write` adds them); `yarn luma`
192
+ lists the tools.
193
+ 1. `yarn luma footprint --rtt 100 --runs 5` in the integration repository, before
194
+ and after the change. Compare the cold and warm `ready`, main thread, scripts,
195
+ bytes, chain and API requests per scenario. Commit the JSON (`--json`) so
196
+ the next change has a baseline.
197
+ 2. `yarn luma tasks`: no task of our own over 16 ms at 4x CPU, per scenario. A
198
+ bot challenge stops the live run and is never worked around: run it on the
199
+ stub page, or ask the client to allowlist the runner.
200
+ 3. On the live site with `?_lbx_env=development`, in DevTools with 4x CPU
201
+ and Fast 4G: a Performance recording of a category page and a search page,
202
+ cold (new profile) and warm. Check the Layout shifts track, the longest task
203
+ in ours, and `__lbx.health.timeline`.
204
+ 4. `yarn luma a11y` and `yarn luma parity`: speed must not cost accessibility or parity
205
+ with the site's listing.
206
+ 5. Phone width, desktop width, and a tall screen (1600 px): the learnt
207
+ layout (facets and quick types above or beside) differs by width, and the
208
+ first screen holds more tiles on a tall one.
209
+
210
+ ## What the site includes
211
+
212
+ The snippet with the connection hints is in README.md ("What the customer
213
+ includes").