Skip to content

Commit 0033b39

Browse files
nextlevelshitMichael Czechowski
authored andcommitted
feat(blog): 5 more posts (webgpu, sw caching, fetchpriority, intl segmenter, web crypto) (#170)
Co-authored-by: Michael Czechowski <mail@dailysh.it> Co-committed-by: Michael Czechowski <mail@dailysh.it>
1 parent 637ec7a commit 0033b39

10 files changed

Lines changed: 952 additions & 0 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
title: "fetchpriority — Tell the Browser What Loads First"
3+
description: "fetchpriority is the one HTML attribute that lifts LCP by 30%+ on hero-heavy pages. Browser-native priority hints, no JS, no preload tricks."
4+
date: 2026-05-10
5+
slug: fetch-priority-hint
6+
tags: [html, performance]
7+
---
8+
9+
The hero image is what your user sees. The browser doesn't know that. By default it requests resources in DOM order, with stylesheets / scripts taking priority over images. Result: your hero image waits in the queue while a tracking script loads. `fetchpriority` fixes this in one HTML attribute.
10+
11+
## The basic case
12+
13+
```html
14+
<img src="/hero.jpg" alt="" fetchpriority="high">
15+
```
16+
17+
That tells the browser: this image is critical, request it before other images and most non-blocking resources. On a typical landing page this lifts **LCP (Largest Contentful Paint) by 200–500ms**.
18+
19+
## Three values
20+
21+
| Value | Behavior |
22+
|-----------|------------------------------------------------|
23+
| `high` | Fetch ASAP, ahead of default-priority resources |
24+
| `low` | Defer until after high+default work |
25+
| `auto` | Default; browser heuristics decide |
26+
27+
## Real wins
28+
29+
### Hero LCP
30+
31+
Without:
32+
```html
33+
<img src="/hero.jpg" alt=""> <!-- loads after CSS, after deferred scripts -->
34+
```
35+
36+
With:
37+
```html
38+
<img src="/hero.jpg" alt="" fetchpriority="high">
39+
```
40+
41+
Chrome dev report from the spec proposal: median LCP improvement 12%, p95 improvement 27% on real-world e-commerce hero pages.
42+
43+
### Below-the-fold images
44+
45+
```html
46+
<img src="/footer-art.jpg" loading="lazy" fetchpriority="low" alt="">
47+
```
48+
49+
Lazy-load AND deprioritize. The browser knows it's below-the-fold (lazy) AND not critical (low priority). It hits the network last.
50+
51+
### Preload + priority
52+
53+
```html
54+
<link rel="preload" as="font" href="/inter.woff2" fetchpriority="high" crossorigin>
55+
```
56+
57+
For preloaded resources you can override their default priority. Useful when you want to preload a font early but not block the LCP image.
58+
59+
## Works with fetch() too
60+
61+
```js
62+
fetch("/api/critical-data", { priority: "high" });
63+
fetch("/api/analytics", { priority: "low" });
64+
```
65+
66+
Same three values. The browser respects the hint when scheduling concurrent requests.
67+
68+
## What it isn't
69+
70+
- **A guarantee.** It's a hint. The browser can ignore it under memory pressure or weird network conditions.
71+
- **Bandwidth control.** All requests still share the same connection budget; high-priority just goes first.
72+
- **A replacement for `loading="lazy"`** for off-screen images. Lazy stops them entirely until needed; `fetchpriority="low"` still loads them, just after.
73+
74+
## Common combos
75+
76+
```html
77+
<!-- LCP image -->
78+
<img src="/hero.jpg" alt="" fetchpriority="high" decoding="async">
79+
80+
<!-- Above-the-fold but not LCP -->
81+
<img src="/sidebar.jpg" alt="">
82+
83+
<!-- Below-the-fold -->
84+
<img src="/footer.jpg" alt="" loading="lazy" fetchpriority="low">
85+
```
86+
87+
```html
88+
<!-- Critical CSS already inlined; this is theme override -->
89+
<link rel="stylesheet" href="/theme.css" fetchpriority="low">
90+
91+
<!-- Web font for hero text -->
92+
<link rel="preload" as="font" href="/hero-font.woff2" fetchpriority="high" crossorigin>
93+
```
94+
95+
## The trap: don't mark everything high
96+
97+
If five things are `high`, none of them is. The browser will load them sequentially in DOM order, defeating the hint.
98+
99+
Pick at most **one image** as `high` per viewport. Maybe two if you have a hero plus an above-fold logo.
100+
101+
## Browser support
102+
103+
- Chrome / Edge 101+ (April 2022)
104+
- Safari 17.2+ (December 2023)
105+
- Firefox 132+ (October 2024)
106+
107+
For older browsers, the attribute is ignored — no harm done. Pure progressive enhancement.
108+
109+
## Pair with the LCP candidate detection
110+
111+
Use Chrome DevTools → Performance → Web Vitals trace to find your actual LCP element. Often it's NOT the obvious hero — it's a banner image, a heading, or a video poster. Mark THAT element with `fetchpriority="high"`.
112+
113+
```js
114+
// In dev console: find your LCP candidate
115+
new PerformanceObserver((list) => {
116+
const lcp = list.getEntries().at(-1);
117+
console.log("LCP:", lcp.element, lcp.size);
118+
}).observe({ type: "largest-contentful-paint", buffered: true });
119+
```
120+
121+
## When to actually skip it
122+
123+
- **Static blog posts with no critical image** — text-LCP isn't fetch-bound
124+
- **Apps where every image is critical** (a gallery): use a different strategy (preload the first 2-3 explicitly)
125+
- **Video-first pages** — fetchpriority on `<video poster>` works but the real LCP is often the first video frame
126+
127+
## Cheap fix vs lasting fix
128+
129+
`fetchpriority="high"` is the cheapest perceived-speed improvement on the platform — one attribute, 200–500ms on real hardware. Most teams discover it after spending months on bundle splitting and caching strategies. Try it first.
130+
131+
---
132+
133+
Practice modern HTML in the [`html-elements`](/html-elements/0/) module on [Code Crispies](/) — covers semantic elements + media attributes.

blog/2026-05-10-intl-segmenter.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
title: "Intl.Segmenter — String.split Was Wrong, Here's the Fix"
3+
description: "Counting characters with .length lies on emoji, accents, CJK, and grapheme clusters. Intl.Segmenter is the platform's text-aware segmentation API."
4+
date: 2026-05-10
5+
slug: intl-segmenter
6+
tags: [javascript, typography]
7+
---
8+
9+
```js
10+
"👨‍👩‍👧".length; // 8
11+
"naïve".split(""); // ["n", "a", "i", "̈", "v", "e"] — accent split off
12+
"今日は".split(/\s/); // ["今日は"] — no spaces between Japanese sentences
13+
```
14+
15+
JavaScript strings are UTF-16 code units. **One visible character ≠ one code unit.** This breaks `.length`, `.split("")`, character counting, truncation, search, line-breaking. `Intl.Segmenter` is the spec-compliant replacement.
16+
17+
## Counting graphemes, not code units
18+
19+
```js
20+
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
21+
[...seg.segment("👨‍👩‍👧")].length; // 1 — one family emoji
22+
[...seg.segment("naïve")].length; // 5 — n, a, ï, v, e (composed correctly)
23+
```
24+
25+
A grapheme is "one user-perceived character." Family emoji = 1. Combined accent = 1. Tonal mark over Vietnamese = 1.
26+
27+
## Truncating without breaking emoji
28+
29+
The classic "first 40 chars" naive code:
30+
31+
```js
32+
text.slice(0, 40); // can split a multi-code-unit char in half
33+
```
34+
35+
Correct version:
36+
37+
```js
38+
function truncate(text, n) {
39+
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
40+
const out = [];
41+
for (const { segment } of seg.segment(text)) {
42+
if (out.length >= n) break;
43+
out.push(segment);
44+
}
45+
return out.join("");
46+
}
47+
48+
truncate("Hello 👨‍👩‍👧 family", 8); // "Hello 👨‍👩‍👧" — emoji intact
49+
```
50+
51+
Never splits a code-unit pair. Never breaks ZWJ-joined emoji.
52+
53+
## Word segmentation (real word boundaries)
54+
55+
`text.split(/\s+/)` works for English/French. It fails for:
56+
57+
- **Japanese / Chinese / Thai** — no spaces between words
58+
- **Arabic** — words separated by spaces but compound forms tricky
59+
- **English contractions** — "don't" should be one or two words depending on use case
60+
61+
```js
62+
const seg = new Intl.Segmenter("ja", { granularity: "word" });
63+
[...seg.segment("今日は良い天気ですね")].filter(s => s.isWordLike).map(s => s.segment);
64+
// ["今日", "は", "良い", "天気", "です", "ね"]
65+
```
66+
67+
The browser uses the locale-specific dictionary. Japanese Mecab-style segmentation, Thai dictionary-based, English standard tokenization — all built in.
68+
69+
## Line breaking
70+
71+
For wrapping text yourself (canvas rendering, custom controls):
72+
73+
```js
74+
const seg = new Intl.Segmenter("en", { granularity: "sentence" });
75+
const sentences = [...seg.segment(longText)].map(s => s.segment);
76+
```
77+
78+
Granularity options: `grapheme | word | sentence`.
79+
80+
Browsers natively use the same algorithm for `text-wrap: balance` and accessibility tools — so when you implement custom line breaking with `Intl.Segmenter`, you match what the browser does for native text.
81+
82+
## Real example: emoji-safe character counter
83+
84+
The "tweet still has 73 characters" UI:
85+
86+
```js
87+
function characterCounter(text, max = 280) {
88+
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
89+
const used = [...seg.segment(text)].length;
90+
return { used, remaining: max - used, valid: used <= max };
91+
}
92+
93+
characterCounter("Hello 👨‍👩‍👧!"); // { used: 8, remaining: 272, valid: true }
94+
```
95+
96+
Twitter/X's API counts the same way. Naive `.length` would say 14 and reject submissions that should pass.
97+
98+
## Fuzzy search with locale-aware boundaries
99+
100+
```js
101+
function findWord(text, query, locale = "en") {
102+
const seg = new Intl.Segmenter(locale, { granularity: "word" });
103+
const words = [...seg.segment(text)]
104+
.filter(s => s.isWordLike)
105+
.map(s => s.segment.toLowerCase());
106+
return words.includes(query.toLowerCase());
107+
}
108+
109+
findWord("the quick brown fox", "fox"); // true
110+
findWord("今日は良い天気ですね", "良い", "ja"); // true (segments correctly)
111+
```
112+
113+
Works across locales without per-language word-split logic.
114+
115+
## Combine with String.prototype.normalize
116+
117+
Some characters have multiple Unicode representations:
118+
119+
```js
120+
"é" === "é"; // false sometimes (one is composed, one is decomposed)
121+
122+
const a = "é".normalize("NFC");
123+
const b = "é".normalize("NFC");
124+
a === b; // true
125+
126+
[...new Intl.Segmenter("en").segment(a)].length; // 1
127+
```
128+
129+
Always normalize before segmenting if input might come from multiple sources (paste from email, OCR output, mixed inputs).
130+
131+
## Performance note
132+
133+
`Intl.Segmenter` is implemented in C++ in the browser — it's fast. The cost is the iterator allocation:
134+
135+
```js
136+
// Slow if called per keystroke on a long text:
137+
[...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(text)];
138+
139+
// Faster: reuse the segmenter
140+
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
141+
function count(text) { return [...seg.segment(text)].length; }
142+
```
143+
144+
Cache the `Segmenter` instance globally; only the iteration is per-call.
145+
146+
## Browser support
147+
148+
- Chrome / Edge 87+ (December 2020)
149+
- Safari 14.1+ (April 2021)
150+
- Firefox 125+ (April 2024 — late but landed)
151+
152+
Universal in 2025. For older Firefox, polyfill exists (`@formatjs/intl-segmenter`).
153+
154+
## What this kills
155+
156+
- `String.prototype.length` for "how many characters" — wrong by default for emoji and combined chars
157+
- `text.split("")` for grapheme iteration — wrong same way
158+
- Per-language word-splitting libraries — replaced by one locale-aware API
159+
- `.split(/\s+/)` for languages without spaces
160+
161+
## What it doesn't do
162+
163+
- **Character-by-character UTF-8 byte iteration** — that's `TextEncoder`, different concern
164+
- **Pinyin / romanization** — that's `Intl.NumberFormat` for digits, but text romanization needs a library
165+
- **Hyphenation** — separate spec (`hyphens: auto` in CSS, no JS API)
166+
167+
## The headline
168+
169+
Every "count characters" code path you've ever written is wrong by default for international text. `Intl.Segmenter` is the one-line fix — write it once, get correct behavior for English, Japanese, Hindi, Arabic, and emoji. The platform finally caught up to what users actually type.
170+
171+
---
172+
173+
Practice JS basics in the [`js-variables`](/js-variables/0/) module on [Code Crispies](/) — covers strings + iteration patterns.

0 commit comments

Comments
 (0)