What this covers
I handled two things in one day. On touch screens: block drag, column resize, and hover-only controls all breaking. And in an app with image optimization turned off: list thumbnails pulling the original image as-is. I organize this task by task — for each, the situation, the goal, how it was solved, and what I learned.
Day summary
| Task | What I wanted | What I did | Result |
|---|---|---|---|
| Gesture | Dragging by touch shouldn't scroll the page | pointerType branching (mouse immediate / touch long-press·threshold), route pointercancel through the same cleanup path as pointerup, touch-action:none on handles, always-show controls on hover-less devices | Fixed scroll hijacking, contextmenu, handle flicker, stuck drag |
| Transfer | Download images only at the size needed | Server proxy: fetch original → convert to WebP → CDN + in-memory two-tier cache, stream misses via tee() | Large drop in transferred bytes on repeat requests for the same image |
| Safety | Proxy shouldn't fetch just any URL | SSRF guard that only fetches allowed hosts, cache key from pathname with signature query stripped | Blocks internal-network addresses, stabilizes the cache key |
| Perceived UX | Reserve space with a blurry preview before the real image | Attach LQIP via SSR on the first card with stale-while-populate (respond first, fill cache behind), preload href = img src | Less empty-slot jank, no wasted preload |
1. Mobile pointer gestures
Background (concepts)
- Pointer Events: a browser API that unifies mouse, touch, and pen into a single event model. Each event carries a
pointerType(mouse/touch/pen). touch-action: CSS that decides how much of the default touch behavior (scroll, zoom) the browser takes on this element. Withnone, the browser doesn't touch it and our code owns the gesture entirely.
What the situation was
Porting desktop pointer drag straight to touch stacks up several symptoms. The gesture gets stolen by scroll; a long press pops the browser right-click menu (contextmenu); hover-only handles flicker on every tap; and if, instead of a lift-off pointerup, only a pointercancel arrives because the browser hijacked the gesture, the drag state gets stuck.
Core work
I reworked it with pointerType branching (mouse immediate / touch long-press·threshold), routing pointercancel through the same cleanup path as pointerup, touch-action:none on handles, and always-visible controls on hover-less devices. As a result scroll hijacking, contextmenu, handle flicker, and stuck drag were all resolved.
Lesson
- Touch gestures aren't a copy-paste of desktop pointers. You must spell out
pointerTypebranching,touch-action, andpointercancelcleanup each explicitly.
→ Pointer Events touch gesture handling
2. Server image proxy
Background (concepts)
- Image proxy: a server that, instead of shipping the original as-is, changes size and format in the middle (transcoding) to deliver something lighter.
- SSRF (Server-Side Request Forgery): a vulnerability where, when the server fetches a URL on your behalf, an attacker supplies an internal-network address to abuse the server as a bypass channel.
- LQIP (Low-Quality Image Placeholder): a tiny, blurry preview image shown briefly before the real image arrives.
- LCP (Largest Contentful Paint): the time until the largest content in the viewport is painted. One of the performance metrics.
What the situation was
In an environment with image optimization turned off, the framework doesn't resize or convert formats, so even a small thumbnail slot gets the whole original. The transferred bytes are needlessly large. On top of that, if the empty slot janks before the image arrives, perceived speed looks worse. And because the server ends up fetching URLs on your behalf, accepting just any URL creates SSRF risk.
Core work
- Transfer: server proxy — fetch original → convert to WebP → CDN + in-memory two-tier cache, stream misses via
tee(). Transferred bytes dropped a lot on repeat requests for the same image. - Safety: an SSRF guard that only fetches allowed hosts, and a cache key from the pathname with the signature query stripped, blocked internal-network addresses and stabilized the cache key.
- Perceived UX: attaching LQIP via SSR on the first card with stale-while-populate (respond first, fill the cache behind), preload href = img src, reduced empty-slot jank and eliminated wasted preload.
Lesson
- In an environment with image optimization off, a server proxy gives the biggest win — the point is that transferred bytes go down.
- For performance comparison, transferred bytes are the most reproducible evidence. LCP measured on localhost has no network latency (RTT), so it tends to undervalue the real-deploy CDN win.
- A blurry placeholder (LQIP) isn't for the score, it's for perceived UX. The browser excludes low-information blur from LCP candidates, so to move the LCP number you must first lighten the real image.
→ Server image proxy — transcoding, cache, streaming · SSRF URL fetch proxy guard · LQIP blur placeholder — SSR stale-while-populate