How I Cut Load Time from ~12s to ~2s: A Longer, More Technical Walkthrough

Reduce TTFB WordPress

This is the same story as before—a site that felt fine on my desk but was painfully slow in the real world—told with more technical depth: what I measured, what I changed, and what I would do again on the next project.


1. Define the problem in numbers, not feelings

“Slow” is vague. Before changing anything, I anchored the work to observable metrics:

MetricWhat it roughly means
TTFB (Time to First Byte)Server + PHP + DB before HTML starts
FCP (First Contentful Paint)First pixels (text, image, etc.)
LCP (Largest Contentful Paint)Largest visible element—often hero image
CLS (Cumulative Layout Shift)Layout jumping while loading
INP (Interaction to Next Paint)How snappy the UI feels after tap/click

Tools I used:

  • Chrome DevTools → Performance + Lighthouse (local, repeatable).
  • Network throttling (Fast 3G / Slow 4G) to simulate real phones.
  • WebPageTest for filmstrip, connection profiles, and waterfall from another region.
  • Coverage tab (unused JS/CSS) as a hint, not a religion.

Lesson: Optimize for LCP + TTFB first on content sites; INP if you have heavy client-side UI.


2. The waterfall told the truth: too much work before “useful”

On a cold load, the waterfall looked like:

  1. HTML waits on slow TTFB.
  2. HTML references many render-blocking CSS/JS files.
  3. Large images compete with critical resources.
  4. Third-party scripts (analytics, chat, maps) extend the “busy” period.

So the plan split into four lanes: server/HTML, critical rendering path, media, and JavaScript.


3. Server and TTFB: make the first byte cheap

TTFB is everything that happens before the first byte of the response. For WordPress (which this story fits well), typical levers:

3.1 PHP execution time

  • Opcache enabled and properly configured (production).
  • Avoid fat functions.php doing heavy work on every request—hook expensive logic to specific contexts (is_admin(), REST, cron, etc.).
  • Autoloaded options audit: SELECT option_name, LENGTH(option_value) FROM wp_options WHERE autoload='yes' ORDER BY LENGTH(option_value) DESC LIMIT 20; — huge autoloaded rows hurt every page.

3.2 Database

  • Object cache (Redis/Memcached) when traffic or query load justifies it—cuts repeated meta/option reads.
  • Query Monitor plugin (staging only) to find slow queries, N+1 patterns, missing indexes on custom tables.
  • Limit post revisions and clean spam comments / transients if the DB is bloated.

3.3 Page caching

For anonymous traffic, full page cache (LiteSpeed, WP Rocket, nginx fastcgi_cache, Cloudflare APO, etc.) turns many requests into “serve static HTML.” That is often the difference between hundreds of ms and single-digit ms TTFB at the edge.

Technical takeaway: If TTFB is high with page cache, the origin is still slow or cache is bypassed (cookies, Woo cart, logged-in users, overly broad “don’t cache” rules).


4. Critical rendering path: CSS, fonts, and “above the fold”

4.1 CSS

Problems I see often:

  • One giant stylesheet with rules for every template.
  • Render-blocking <link rel="stylesheet"> in <head> with no strategy.

What actually helped:

  • Split critical vs non-critical mentally: ensure the hero, typography, and layout for first viewport are available early.
  • Avoid @import chains in CSS—they serialize downloads.
  • HTTP/2 reduces the pain of multiple files, but too many small CSS files still adds overhead—balance merge vs cache granularity.

Reality check: “Critical CSS inlined” is powerful but high maintenance on WordPress unless automated in the build or by a serious optimization plugin. I preferred fewer blocking sheets + smaller global CSS over fragile per-page critical CSS for this project.

4.2 Fonts

Webfonts were a hidden tax:

  • Multiple families × weights = multiple files + layout shifts.

Technical fixes:

  • Reduced to one family (or one display + one body) and two–three weights max.
  • font-display: swap so text shows immediately with fallback, then swaps.
  • Preconnect to fonts.gstatic.com when using Google Fonts; self-host fonts for full control and fewer third-party connections.
  • Subset fonts (Latin only if you do not need extended scripts).

@font-face {

font-family: “Body”;

src: url(“/fonts/body.woff2”) format(“woff2”);

font-weight: 400 700;

font-display: swap;

}


5. Images: the single biggest front-end win

Images dominated LCP and total bytes.

5.1 Right-sized assets

  • Serve width/height that match display, not 4000px originals in a 400px slot.
  • Use srcset + sizes for responsive images (WordPress does this for wp_get_attachment_image when using core functions).

<img

src=”hero-800.webp”

srcset=”hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w”

sizes=”(max-width: 600px) 100vw, 1200px”

width=”1200″

height=”630″

alt=”…”

/>

5.2 Modern formats

  • WebP broadly; AVIF where acceptable trade-offs on encoding time exist.
  • WordPress 5.8+ can generate WebP depending on server support; plugins can help on older stacks.

5.3 Lazy loading and LCP exception

  • loading="lazy" on below-the-fold images.
  • Do not lazy-load the LCP image—it delays the metric WordPress and Google care about. In WordPress 5.9+, the first content image in a post may get fetchpriority="high" in some contexts; for custom themes, set it explicitly on the hero.

<img src=”hero.webp” fetchpriority=”high” decoding=”async” width=”…” height=”…” alt=”…” />

5.4 CLS: dimensions matter

Always width and height attributes (or CSS aspect-ratio) so the browser reserves space before decode. CLS dropped noticeably after fixing “mystery jumps” from late-loading media.


6. JavaScript: defer, delete, and isolate third parties

6.1 Loading strategy

  • Default scripts without defer/async block HTML parsing when placed traditionally.
  • Prefer defer for order-dependent scripts; async for independent snippets (analytics sometimes—trade accuracy vs blocking).
  • Move non-critical JS to footer or load after interaction (“load maps when user opens map”).

<script src=”/app.js” defer></script>

6.2 Dependency diet

  • Removed duplicate libraries (multiple jQuery versions, overlapping sliders).
  • Replaced “one plugin for a tiny UI feature” with a few lines of native code where reasonable.

6.3 Third-party scripts

Each vendor script is a DNS + TLS + download + parse + main-thread cost.

Technical mitigations:

  • Tag Manager with strict triggers (load on scroll / click / timer only if acceptable).
  • Partytown or web worker offloading for some analytics (advanced; not always worth it).
  • facade pattern: show a static poster for embedded video/maps until click.

7. Caching layers (stack them, do not duplicate blindly)

A sensible production stack often has:

  1. Browser cache via Cache-Control / ETag for static assets with hashed filenames.
  2. CDN edge (Cloudflare, Bunny, etc.) for static files and optionally HTML.
  3. Origin page cache for WordPress HTML.
  4. Object cache for DB hot paths.

Technical pitfall: aggressive “minify + combine” breaking CSS order or deferred CSS causing FOUC. Always verify LCP element, mobile menu, and forms after optimization changes.


8. WordPress-specific checklist (practical)

If your site is WordPress, this is a strong order of operations:

  1. Page cache for anonymous users; exclude cart/checkout/account as needed.
  2. Image pipeline: WebP/AVIF, proper sizes, lazy except LCP.
  3. Plugin audit: deactivate suspects on staging, compare TTFB and main-thread time.
  4. Database: autoload bloat, revisions, cron health (wp cron event list).
  5. Theme: remove unused block/CSS from builders; fewer webfont weights.
  6. CDN for wp-content/uploads and static theme assets.

Debugging command (WP-CLI):

wp plugin list –status=active

wp transient list –search=”*_timeout” –format=count

(Exact transient commands vary by WP-CLI version; use as a pattern, not gospel.)


9. What “12s → 2s” actually meant in my case

Honesty matters for technical readers:

  • 12s was “time until the page felt usable” on throttled mobile—including third-party widgets and unoptimized hero media, not just TTFB.
  • 2s was the same definition after image work + script deferral + caching + removing worst plugins.

If you only look at Lighthouse “Performance” on cable Wi‑Fi, you can fool yourself. I reported numbers with throttling and repeat view (cached) separately.


10. If I were doing it again: a repeatable playbook

  1. Filmstrip + LCP node (what is LCP?).
  2. TTFB (origin vs edge; cache hit?).
  3. Image bytes + lazy flags for LCP candidate.
  4. Main-thread long tasks (JS).
  5. Layout shifts (dimensions, fonts, dynamic ads).
  6. Ship, then regressions guard: periodic Lighthouse CI or RUM (Cloudflare Web Analytics, SpeedCurve, etc.).

Closing

The technical story is mostly measurement, images, render-blocking resources, caching, and third-party control. Framework rewrites rarely beat disciplined asset and request hygiene. Use the metrics above as guardrails, ship incremental fixes, and validate on real devices and networks—that is how a dramatic “12s to 2s” story stays honest and repeatable.

Share this post