Frontend UX Playbook — Skeletons, Optimistic UI, Lazy Loading, Caching (Svelte/HTML/Tailwind/daisyUI)
This “what it is + how to” cheat‑sheet rounds up the patterns you see at big shops—implemented simply with Svelte, vanilla HTML, Tailwind, and daisyUI.
1) Skeleton Screens
What it is: Placeholder UI that mimics the final layout while data loads. Reduces perceived wait time and layout shift.
When to use: Lists, profile cards, dashboards, anywhere remote data drives the view.
<!-- Svelte: skeleton component -->
<!-- src/lib/SkeletonCard.svelte -->
<script> export let rows = 2; </script>
<div class="flex gap-3 items-start">
<div class="skeleton w-12 h-12 rounded-lg"></div>
<div class="flex-1">
{#each Array(rows) as _}
<div class="skeleton h-3 my-2"></div>
{/each}
</div>
</div>
<style>
/* If not using daisyUI .skeleton class, a minimal fallback: */
.skeleton { background: #e6e6e6; border-radius: .5rem; }
@media (prefers-reduced-motion: no-preference) {
.skeleton { animation: pulse 1.2s ease-in-out infinite; }
@keyframes pulse { 0%{opacity:.7}50%{opacity:1}100%{opacity:.7} }
}
</style>
<!-- Svelte page using {@await} -->
<script>
import SkeletonCard from '$lib/SkeletonCard.svelte';
export let dataPromise; // Promise that resolves to items
</script>
{@await dataPromise}
<SkeletonCard rows={3} />
{:then items}
{#each items as item}
<article class="card bg-base-100 shadow p-4">
<h3 class="text-lg font-semibold">{item.title}</h3>
<p class="text-sm opacity-80">{item.summary}</p>
</article>
{/each}
{:catch e}
<p class="text-error">Failed: {e.message}</p>
{/await}
<!-- HTML + Tailwind/daisyUI only -->
<div class="skeleton w-48 h-6 mb-2"></div>
<div class="skeleton w-80 h-3 mb-2"></div>
<div class="skeleton w-64 h-3"></div>
Pitfalls: Too many placeholders = visual noise. Size skeletons to match final layout to avoid jumpiness.
2) Optimistic UI
What it is: Update the screen immediately, then reconcile with the server. On error, roll back. Makes apps feel instant.
// Svelte store with optimistic add + rollback
import { writable } from 'svelte/store';
export const todos = writable([]);
export async function addTodoOptimistic(text) {
const id = crypto.randomUUID();
const optimistic = { id, text, done: false, _optimistic: true };
todos.update(t => [optimistic, ...t]);
try {
const res = await fetch('/api/todos', {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id, text })
});
if (!res.ok) throw new Error('rejected');
const saved = await res.json();
todos.update(t => t.map(x => x.id === id ? { ...saved, _optimistic:false } : x));
} catch (e) {
todos.update(t => t.filter(x => x.id !== id)); // rollback
throw e;
}
}
<!-- HTML + Tailwind: mark optimistic rows -->
<li class="flex items-center gap-2 data-[optimistic=true]:opacity-60" data-optimistic="true">
<input type="checkbox" class="checkbox checkbox-sm" disabled />
<span>New todo (saving…)</span>
</li>
Tips: Use client‑generated IDs to match server responses; style `_optimistic` items (opacity) to set expectations.
3) Lazy Loading (components, images, code)
What it is: Defer loading heavy chunks until they’re needed or visible.
<!-- Svelte: lazy import when visible -->
<script>
let Chart, visible = false;
async function onVisible() {
if (!Chart) {
const m = await import('$lib/BigChart.svelte'); Chart = m.default;
}
visible = true;
}
function intersect(node) {
const io = new IntersectionObserver(([e]) => e.isIntersecting && (onVisible(), io.disconnect()), { rootMargin: '200px' });
io.observe(node); return { destroy() { io.disconnect(); } };
}
</script>
<div use:intersect style="min-height:320px">
{#if visible && Chart}<Chart />{:else}<div class="skeleton h-64 w-full"></div>{/if}
</div>
<!-- Images: native lazy loading -->
<img src="/img/hero-640.jpg" width="640" height="360" loading="lazy" decoding="async" alt="Hero">
<!-- HTML preload/prefetch hints -->
<link rel="preload" as="image" href="/img/hero-640.jpg">
<link rel="prefetch" href="/next/page" as="document">
Pitfalls: Don’t lazy‑load above‑the‑fold hero images; it can hurt LCP. Pre-size containers to avoid CLS.
4) Client‑side Caching (SWR: Stale‑While‑Revalidate)
What it is: Return cached data instantly, then re‑fetch in the background and update.
// Tiny SWR-style cache helper
const cache = new Map(); // key -> { ts, ttl, data, inflight }
export async function fetchCached(key, ttlMs, loader) {
const now = Date.now();
const hit = cache.get(key);
if (hit && now - hit.ts < hit.ttl) return hit.data;
if (hit?.inflight) return hit.inflight;
const p = loader().then(data => (cache.set(key, { ts: now, ttl: ttlMs, data }), data))
.finally(() => { const h = cache.get(key); if (h) h.inflight = null; });
cache.set(key, { ...(hit||{}), ts: now, ttl: ttlMs, inflight: p });
return hit?.data ?? p;
}
// Svelte usage
import { fetchCached } from '$lib/cache.js';
$: products = await fetchCached('products', 30_000, () => fetch('/api/products').then(r=>r.json()));
Tip: Add jitter to TTLs to avoid thundering herds; expose a `force` option to bypass cache after mutations.
5) Prefetch on Hover / In‑View
What it is: Download the next page’s code/data when the user is likely to need it.
// Svelte: simple hover prefetch for JSON
function prefetch(url) { return fetch(url, { credentials: 'include' }).then(()=>{}).catch(()=>{}); }
<a href="/products" on:mouseenter={() => prefetch('/api/products?prefetch=1')}>Products</a>
<!-- HTML link hint -->
<link rel="prefetch" href="/products" as="document">
Pitfalls: Be respectful on mobile/data caps; throttle prefetching and skip on `save-data` clients.
6) Debounce & Throttle
What it is: Rate-limit noisy UI events (typing/scroll) to reduce reflows and network chatter.
// Debounce helper
export const debounce = (fn, ms=200) => {
let t; return (...args) => { clearTimeout(t); t = setTimeout(()=>fn(...args), ms); };
};
<!-- Svelte: debounced search -->
<script>
import { debounce } from '$lib/debounce.js';
let q = '';
const onInput = debounce(async (v) => {
const res = await fetch('/api/search?q=' + encodeURIComponent(v));
// render results...
}, 250);
</script>
<input class="input input-bordered w-full" placeholder="Search" bind:value={q} on:input={(e)=>onInput(e.target.value)} />
7) Image Placeholders (blur‑up / dominant color)
What it is: Show a tiny blurred preview or solid color matching the image while the real image loads. Reduces perceived wait + CLS.
<!-- HTML blur-up technique -->
<div class="relative overflow-hidden w-[320px] h-[180px] bg-[#e3d5c3]">
<img src="/img/preview-20w.jpg" class="absolute inset-0 w-full h-full object-cover blur-sm" aria-hidden="true">
<img src="/img/full-1280w.jpg" class="absolute inset-0 w-full h-full object-cover" loading="lazy" decoding="async" alt="">
</div>
8) Abortable Fetch + Retry
What it is: Cancel stale requests and back off on failure for resilient UIs.
export async function getJSON(url, { signal, tries=2 } = {}) {
for (let i=0; i<=tries; i++) {
try {
const r = await fetch(url, { signal });
if (!r.ok) throw new Error(r.statusText);
return await r.json();
} catch (e) {
if (e.name === 'AbortError' || i === tries) throw e;
await new Promise(res => setTimeout(res, 300 * (i+1))); // backoff
}
}
}
// Usage with AbortController
const ac = new AbortController();
const p = getJSON('/api/items', { signal: ac.signal });
// later, if user navigates away:
ac.abort();
9) Progressive Enhancement & No‑JS Fallbacks
What it is: Core content and actions should work without JS; JS enhances UX (optimistic UI, transitions).
<!-- HTML form that works without JS; JS can 'enhance' it later -->
<form action="/subscribe" method="POST" class="flex gap-2">
<input class="input input-bordered" name="email" type="email" required placeholder="you@example.com" />
<button class="btn btn-primary">Subscribe</button>
</form>
10) Accessibility & Motion
What it is: Respect user settings and keep skeleton/transition effects subtle.
/* Tailwind config utilities already help, but add this too */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}
Measure: Track Web Vitals (LCP, CLS, INP) and real‑user metrics. Confirm skeletons improve perceived speed without tanking LCP.
Grab bag checklist
□ Skeletons for remote data • □ Optimistic on likely success
□ Lazy‑load heavy components/images • □ Prefetch on hover/in‑view
□ Client cache (SWR) • □ Debounce noisy inputs
□ Abort stale fetches • □ Pre-size images to avoid CLS
□ Respect reduced motion • □ Monitor Web Vitals
Comments
Post a Comment