Word and Character Counts for UI Copy, SEO, and Reviews
How to count words and characters the way products and platforms do — limits, diffs, Markdown pitfalls, and a practical editing workflow for developers who ship copy.
A practical guide to frontend performance — LCP, INP, CLS explained, with optimization techniques for images, JavaScript, fonts, and rendering that improve both user experience and search rankings.
By codefunc
Performance is user experience measured in milliseconds. A page that loads in 1 second feels instant. A page that loads in 4 seconds feels broken. Google measures this too — Core Web Vitals are ranking factors. But performance optimization is not about gaming metrics; it is about respecting users' time and device capabilities.
Bottom line: Core Web Vitals measure loading (LCP), interactivity (INP), and visual stability (CLS). Fix LCP by optimizing images and critical resources. Fix INP by reducing JavaScript work and breaking up long tasks. Fix CLS by reserving space for dynamic content. Measure with real user data, not just lab tests.
Google's Core Web Vitals are three metrics that represent user experience:
| Metric | Measures | Good | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading | ≤ 2.5s | > 4s |
| INP (Interaction to Next Paint) | Interactivity | ≤ 200ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability | ≤ 0.1 | > 0.25 |
These affect search rankings and user behavior. Users abandon slow pages.
LCP measures when the largest visible content element renders — usually a hero image, main heading, or large text block.
What triggers LCP:
<img> elements<image> inside SVGCommon LCP problems:
INP replaced FID (First Input Delay) in 2024. It measures responsiveness throughout the page lifetime, not just first interaction.
What INP measures:
Common INP problems:
CLS measures unexpected movement of visible content. Every time an element shifts without user interaction, it accumulates.
What causes layout shifts:
| Tool | Use for |
|---|---|
| Lighthouse | Comprehensive audit in DevTools |
| DevTools Performance panel | Flame charts, detailed timing |
| WebPageTest | Filmstrip, waterfalls, comparison |
| PageSpeed Insights | Lab + field data combined |
| Tool | Use for |
|---|---|
| Chrome UX Report (CrUX) | Real user data at origin level |
| Search Console | Core Web Vitals report per URL |
| Web Vitals library | Custom RUM implementation |
Important: Lab and field results differ. Lab tests use consistent conditions; field data reflects real devices, networks, and usage patterns. Optimize for field data.
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
Send to your analytics for real user monitoring (RUM).
| Technique | Impact |
|---|---|
| Use a CDN | Serve from edge locations |
| Cache HTML | Static pages, stale-while-revalidate |
| Optimize backend | Database queries, API calls |
| Use HTTP/2 or HTTP/3 | Multiplexing, header compression |
For static sites, TTFB is usually fast. Dynamic SSR needs more attention.
<!-- Defer non-critical JS -->
<script src="analytics.js" defer></script>
<!-- Async for independent scripts -->
<script src="widget.js" async></script>
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
</style>
<!-- Load full CSS asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
Images are the most common LCP element.
| Technique | Implementation |
|---|---|
| Modern formats | WebP, AVIF instead of PNG/JPEG |
| Responsive images | srcset and sizes attributes |
| Lazy loading | loading="lazy" for below-fold |
| Preload hero image | <link rel="preload" as="image"> |
| Proper sizing | Don't serve 4000px image for 400px slot |
<!-- Responsive image with modern format -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img
src="hero.jpg"
alt="Hero image"
width="1200"
height="630"
fetchpriority="high"
>
</picture>
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">
<link rel="preconnect" href="https://api.example.com">
If your LCP element depends on JavaScript to render, users wait for:
Use SSR or SSG for LCP content. Hydrate interactivity after.
The browser's main thread handles JavaScript, layout, paint, and user input. Long tasks (>50ms) block everything.
// Bad: one long task
function processLargeArray(items) {
items.forEach(item => heavyProcessing(item));
}
// Better: yield to main thread
async function processLargeArray(items) {
for (const item of items) {
heavyProcessing(item);
// Yield every 5ms
if (performance.now() - start > 5) {
await scheduler.yield(); // or setTimeout
start = performance.now();
}
}
}
requestIdleCallback for non-urgent workrequestIdleCallback(() => {
// Analytics, prefetching, non-critical updates
sendAnalytics();
});
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
input.addEventListener('input', debounce(handleSearch, 150));
// Bad: read then write in loop (layout thrashing)
elements.forEach(el => {
const height = el.offsetHeight; // read (forces layout)
el.style.height = height + 10 + 'px'; // write
});
// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // all writes
});
Third-party scripts (ads, analytics, widgets) often cause INP issues.
| Technique | Implementation |
|---|---|
| Load async/defer | <script async> |
| Use facade patterns | Load on interaction |
| Set resource hints | <link rel="preconnect"> |
| Audit regularly | Lighthouse third-party summary |
<!-- CLS-safe image -->
<img src="photo.jpg" alt="..." width="800" height="600">
<!-- Or use aspect-ratio in CSS -->
<style>
.thumbnail {
aspect-ratio: 16 / 9;
width: 100%;
}
</style>
.ad-slot {
min-height: 250px;
background: var(--muted);
}
.video-embed {
aspect-ratio: 16 / 9;
}
/* Prevent FOIT (invisible text) */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
}
Better yet, use font-display: optional for non-critical fonts — they only show if already cached.
// Bad: prepend shifts everything down
container.prepend(newElement);
// Better: append to end or use fixed position
container.append(newElement);
// Or animate in with transforms (no layout shift)
newElement.style.transform = 'translateY(-100%)';
requestAnimationFrame(() => {
newElement.style.transform = 'translateY(0)';
});
contain for isolated components.card {
contain: layout; /* Layout changes don't affect siblings */
}
Load only what's needed for the current page:
// Dynamic import
const module = await import('./heavy-feature.js');
// React lazy loading
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
Import only what you use:
// Bad: imports entire library
import _ from 'lodash';
// Good: imports only needed function
import debounce from 'lodash/debounce';
Reduce file size by removing whitespace, shortening names, and eliminating dead code.
For quick minification of individual files, use:
Production builds should use bundler minification (Vite, webpack, esbuild).
srcsetwidth and height attributesFor small icons and inline images, base64 encoding can reduce HTTP requests. Use the Image Base64 Encoder for assets under 10KB.
Set limits and enforce them in CI:
| Metric | Budget |
|---|---|
| Total JS | < 200KB gzipped |
| Total CSS | < 50KB gzipped |
| LCP | < 2.5s |
| INP | < 200ms |
| CLS | < 0.1 |
| Total page weight | < 1MB |
Tools like Lighthouse CI can fail builds that exceed budgets.
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
page: window.location.pathname,
});
navigator.sendBeacon('/analytics', body);
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
Check Core Web Vitals report weekly:
next/image for automatic optimizationnext/font for optimized font loadingexperimental.optimizeCssReact.lazy() for code splittinguseDeferredValue for non-urgent updatesPerformance optimization is iterative. Start with the biggest impact: fix LCP with image optimization and critical resource loading. Fix INP by auditing JavaScript and breaking up long tasks. Fix CLS by setting explicit dimensions. Measure with real user data, not just lab tests. Set budgets and enforce them in CI. Performance is not a one-time project — it is an ongoing practice.
How to count words and characters the way products and platforms do — limits, diffs, Markdown pitfalls, and a practical editing workflow for developers who ship copy.
Shrink SVG icons for the web — strip comments and whitespace, choose inline vs data URLs, and know when you still need a full SVGO pipeline.
Ship the right favicon and apple-touch-icon sizes, wire Web App Manifest icons, set theme-color, and avoid the common gaps that leave tabs and home screens looking broken.
Find a developer tool