helldots 0.7.0 → 0.9.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/README.md CHANGED
@@ -12,6 +12,10 @@ widget persist to `localStorage`.
12
12
  npm install helldots
13
13
  ```
14
14
 
15
+ > `modern-screenshot` (the screenshot renderer) is a peer dependency. npm 7+,
16
+ > pnpm 8+ and bun install it automatically; on Yarn, add it explicitly with
17
+ > `yarn add modern-screenshot`.
18
+
15
19
  ## Quick start
16
20
 
17
21
  ```js
@@ -186,6 +190,95 @@ Three ways out, cheapest first:
186
190
  - **Leave it.** Captures of such a page stay misaligned where text is
187
191
  concerned; everything else about them is correct.
188
192
 
193
+ ### When a capture is slow
194
+
195
+ A screenshot is not a screenshot. The browser does not hand that to
196
+ JavaScript, so the renderer re-creates the page instead: it clones the DOM,
197
+ reads every element's computed style, inlines all of it, serialises the
198
+ result into an SVG `<foreignObject>` and rasterises that.
199
+
200
+ Reading the styles is the render. A browser exposes around 527 computed
201
+ properties per element, and the renderer reads all of them — on a page with
202
+ a few thousand live nodes that is hundreds of thousands of property reads,
203
+ and it is ~91% of the cost. Two things follow, in this order:
204
+
205
+ - **Nothing waits for it.** Clicking or dragging places the marker and opens
206
+ the comment box immediately; the render runs behind it and the images drop
207
+ in when they land. A dragged region shows a "Capturing…" slot in the
208
+ attachment strip until its crop arrives, and Send waits for it if you get
209
+ there first. This is always on; there is nothing to configure.
210
+ - **The page no longer freezes while it happens.** The capture hands the
211
+ main thread back to the browser every 8 ms, so the page keeps painting and
212
+ accepting keystrokes even on a render that runs for a second. Also always
213
+ on — and it is what makes the point above worth anything, since a box you
214
+ can see but cannot type into is not much better than no box.
215
+ - **`fastCapture: true`** narrows those reads to a curated list of the
216
+ properties that change a pixel — measured at ~2.7x off that phase on a
217
+ 12 000-element page, and pixel-identical to a full capture on the pages it
218
+ was verified against.
219
+
220
+ `fastCapture` is off by default because the list is a fidelity contract, and
221
+ no list is provably complete for a page this library has never seen: a
222
+ property it does not name is simply absent from the image. Turn it on for a
223
+ heavy page, look at one capture before trusting it, and open an issue if
224
+ something comes out wrong — the fix is one more entry in the list.
225
+
226
+ There is a third lever if your page embeds same-origin iframes.
227
+ **`skipIframeContent: true`** renders them blank instead of cloning what is
228
+ inside. An iframe's cost is invisible from the outside — the renderer walks
229
+ into the frame and clones its whole document, so a page that reports 242
230
+ elements can be a capture of 9 245. Measured at 2374 ms against 82 ms on one
231
+ 9 000-node embedded frame.
232
+
233
+ The `<iframe>` element itself is kept: its box, its border, the space it
234
+ occupies. That matters more than it sounds — removing the element instead
235
+ would slide everything below it up by the frame's height while the crop is
236
+ still taken at live page coordinates, which puts the bottom of every capture
237
+ out of register.
238
+
239
+ A cross-origin frame has nothing to gain here. The renderer cannot read into
240
+ it, so it is already blank in the output, and contrary to a common guess it
241
+ does not stall or wait on one either.
242
+
243
+ What none of them touches is rasterisation, which is a single browser
244
+ operation with no JavaScript inside it. On a very long page that stays a
245
+ few hundred milliseconds of unavoidable work.
246
+
247
+ ### When one dead asset holds a capture up
248
+
249
+ To inline the page's images and fonts the renderer re-fetches them, and gives
250
+ each one 30 seconds before giving up. A URL that never answers stalls the
251
+ capture until that fires. The capture still succeeds — that asset becomes a
252
+ transparent placeholder — but it waits first.
253
+
254
+ The wait is bounded rather than multiplied: one dead asset and ten cost the
255
+ same, because they are waited on concurrently. What it is not is one times the
256
+ timeout. The setting drives two waits in sequence on the same asset — first
257
+ for the image already on the page to finish loading, then for the fetch that
258
+ inlines it — so the real cost is a consistent ~2x. The 30 second default is
259
+ therefore about a minute.
260
+
261
+ **`captureTimeout: 5000`** cuts that to roughly ten seconds. It is left at
262
+ the default because lowering it trades a slow capture for a silently
263
+ incomplete one: an asset that was only slow, rather than dead, gets dropped
264
+ and leaves a hole with nothing to say so. Since these are assets your page
265
+ has already loaded, most come from cache instantly and the tail is exactly
266
+ the large or uncached ones you would be wrong to drop. Set it if you have
267
+ measured your own page and decided which way you would rather it failed.
268
+
269
+ ### Very long pages
270
+
271
+ Browsers cap how large a canvas can be — 65 535 pixels in a dimension in
272
+ Chromium, less in Firefox, and a separate and much lower area cap on mobile
273
+ Safari. A page past that cap cannot be rendered at full scale, so HellDots
274
+ fits the scale to what the browser will actually paint and checks that the
275
+ result holds pixels before using it.
276
+
277
+ Nothing below the cap changes. Past it the capture goes soft in proportion:
278
+ a 68 000px page renders at 0.96, a 140 000px page at 0.47. If even the
279
+ smallest attempt comes back empty, the capture fails through `onError`
280
+ rather than attaching a blank image.
281
+
189
282
  ### Keeping screenshots out of your database
190
283
 
191
284
  Every image is stored as a base64 data URL inside the record — around 33 KB
@@ -475,6 +568,9 @@ OS: iOS 17.2
475
568
  | `persistence` | `"localStorage"` \| `"none"` | `"none"` | Auto save/restore, or handle it yourself via callbacks |
476
569
  | `autoScreenshot` | `boolean` | `true` | Capture a screenshot and environment snapshot per comment |
477
570
  | `embedCrossOriginFonts` | `boolean` | `false` | Fetch unreadable stylesheets so their web fonts reach the capture |
571
+ | `fastCapture` | `boolean` | `false` | Read a curated style list instead of all ~527 computed properties |
572
+ | `skipIframeContent` | `boolean` | `false` | Render embedded documents as blank instead of cloning them |
573
+ | `captureTimeout` | `number` | `30000` | Milliseconds one remote asset may hold a capture up |
478
574
  | `locale` | `string` | browser language | `"en"` and `"es"` ship; anything else falls back per key |
479
575
  | `linkParam` | `string` | `"helldotsComment"` | Query param used by "Copy link" URLs |
480
576
  | `navigate` | `(page: string) => void` | full page load | SPA router hook for the widget's cross-page jumps |