Mobile users don’t “browse.” They tolerate. Your WordPress site gets about two seconds to prove it’s not wasting their time, and then the thumb goes back to the search results. Desktop might look fine because desktop hides sins: faster CPUs, better networks, laxer expectations.
When WordPress is slow on mobile, people flail: install three caching plugins, compress everything twice, blame the host, blame the theme, blame “Google.” Don’t do that. Treat it like production troubleshooting: measure, isolate the bottleneck, fix the first constraint, repeat.
Fast diagnosis playbook (find the bottleneck fast)
If you do nothing else, do this. Mobile slowness is usually one of three things: slow server response (TTFB), heavy payload (images/fonts/JS/CSS), or third-party chaos (ads, trackers, chat widgets). The trick is figuring out which one in minutes, not weeks.
Step 1: Decide if it’s server time or browser time
- Check TTFB from a cold request. If TTFB is consistently high (> 600–800ms) on mobile networks, fix server-side first: caching, PHP-FPM, database, backend calls, origin latency.
- If TTFB is fine but LCP is bad, you’re in front-end territory: images, render-blocking CSS, JS bloat, fonts, layout shifts.
- If TTFB is fine sometimes and awful other times, suspect cache misses, backend contention, cron, or a noisy neighbor (CPU/I/O saturation).
Step 2: Confirm caching is actually happening
- If you think you have caching but every request hits PHP, you don’t have caching. You have hope.
- Look for cache headers, upstream cache status, and server logs that show fast/static responses.
Step 3: Find the biggest bytes on the critical path
- On mobile, images and JS are the usual felons.
- Check the LCP element: if it’s a hero image, fix it before you argue about database indexes.
- Kill third-party scripts that block main thread or add long tasks. Most “marketing must-have” scripts are optional once you show the performance cost.
Step 4: Validate with one metric, then move on
Pick one success metric per iteration: TTFB, LCP, INP (interaction latency), or total transferred bytes. Don’t try to fix everything at once; you’ll ship a pile of changes and learn nothing.
Why mobile “slow” is different (and why you should care)
Mobile is not just “smaller desktop.” It’s a different physics engine.
- CPU is weaker, and browsers throttle aggressively to save battery.
- Network latency dominates. Even when bandwidth looks decent, round trips are expensive. Every DNS lookup, TLS handshake, and redirect hurts.
- Memory pressure is real. Big JavaScript bundles and heavy themes can make the browser GC (garbage collect) at the worst times.
- Input is touch. You can “get away” with slow rendering on desktop; you can’t get away with laggy taps.
Also: you’re not optimizing for a lab. You’re optimizing for the messy median user with 12 tabs open, a flaky radio link, and a phone that’s seen better years.
One quote worth keeping on a sticky note: John Allspaw’s paraphrased idea: “Reliability comes from how systems behave under real conditions, not from how they behave in ideal tests.”
Joke #1: Performance work is like dieting—nobody wants to count calories, but everyone wants the beach photos.
Interesting facts and historical context
Some context helps you stop repeating the industry’s greatest hits.
- WordPress launched in 2003 as a fork of b2/cafelog, long before “mobile-first” was a thing. Many default patterns were born in a desktop web era.
- HTTP/2 (standardized in 2015) reduced the pain of many small requests, but it did not make unbounded JavaScript or giant images magically cheap.
- Google introduced Core Web Vitals in 2020 to quantify user experience, and it pushed performance from “nice” to “budget line item.”
- JPEG has existed since the early 1990s; modern formats like WebP (2010) and AVIF (around 2019 adoption wave) meaningfully reduce bytes for photos on mobile.
- Mobile traffic surpassed desktop years ago for many verticals; “we’re mostly desktop” is often based on an outdated analytics view or internal bias.
- CDNs started as static asset accelerators but evolved into full edge platforms with caching, WAF, image resizing, and bot management—useful for WordPress, but easy to misconfigure.
- WordPress’s admin-ajax.php pattern became a common performance pain point as themes/plugins leaned on it for “live” features; many sites accidentally DDOS themselves.
- Lazy-loading moved from hacky JS to native with
loading="lazy", but it can still be misused and delay LCP if you lazy-load the hero image. - PHP 7 (2015) was a performance leap; PHP 8+ adds improvements and JIT (mostly irrelevant for typical WordPress), but the big gains come from opcode caching and avoiding work.
The only mental model you need: TTFB vs render
Mobile slowness complaints usually boil down to: “I tapped and it stared at me.” That stare is either the server not sending bytes (TTFB), or the browser choking on what it got (render/CPU).
Bucket A: High TTFB (server is the bottleneck)
Typical causes:
- No page cache (or it’s bypassed for mobile / logged-in users / query strings).
- Slow PHP execution due to heavy plugins, remote calls, or object-cache misses.
- Database latency (bad queries, missing indexes, overloaded MySQL, slow storage).
- Origin far from users and no CDN cache.
- Rate limiting or WAF challenges that add latency to mobile clients.
Bucket B: Low TTFB but slow paint (front-end is the bottleneck)
Typical causes:
- LCP image too big, unoptimized, served without responsive sizes.
- Render-blocking CSS and font loading delays.
- JavaScript bundles too large, too many third-party scripts, long tasks.
- Too many DOM nodes from page builders and “do everything” themes.
Bucket C: It depends (intermittent)
If the problem comes and goes, assume a cache-miss storm, periodic cron work, traffic spikes, or resource contention. Intermittent performance issues are rarely fixed by “minify JS” and almost always fixed by removing variability: caching, capacity, and limits.
Hands-on tasks: commands, outputs, decisions (12+)
These are production-grade tasks. Each includes a command, what you’re looking at, and what you decide next. Run them on your WordPress host or a staging clone. If you can’t run commands (managed hosting), ask your provider to run equivalents—or move somewhere you can observe your own system.
Task 1: Measure TTFB and total time from the origin
cr0x@server:~$ curl -s -o /dev/null -w "dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total} size:%{size_download}\n" https://example.com/
dns:0.012 connect:0.034 tls:0.081 ttfb:0.742 total:1.214 size:185432
What it means: ttfb is time until first byte; total is request completion; size is bytes returned.
Decision: If TTFB is > ~0.6–0.8s consistently, stop arguing about image formats and fix server-side caching and backend latency first.
Task 2: Compare cache hit vs miss using headers
cr0x@server:~$ curl -sI https://example.com/ | egrep -i "cache|age|cf-cache-status|x-cache|x-fastcgi-cache|server|vary"
server: nginx
vary: Accept-Encoding
cache-control: max-age=0, no-cache, no-store, must-revalidate
x-cache: MISS
What it means: This response is not cache-friendly and shows a miss (or no cache).
Decision: For public pages, you generally want cacheable headers and a visible cache status of HIT at the edge and/or origin. If you can’t get HITs, mobile will suffer first.
Task 3: Confirm whether WordPress is generating the page (PHP hit) or serving static HTML
cr0x@server:~$ tail -n 5 /var/log/nginx/access.log
203.0.113.10 - - [27/Dec/2025:10:11:22 +0000] "GET / HTTP/2.0" 200 185432 "-" "Mozilla/5.0" "upstream_response_time=0.690 request_time=1.201"
203.0.113.11 - - [27/Dec/2025:10:11:23 +0000] "GET /wp-content/uploads/2025/12/hero.jpg HTTP/2.0" 200 742193 "-" "Mozilla/5.0" "-"
What it means: The homepage request has an upstream response time (~0.69s) which likely means it went to PHP-FPM. The image is served directly (no upstream).
Decision: If your public pages show upstream times regularly, implement a full-page cache (plugin cache, Nginx fastcgi_cache, Varnish, or CDN HTML caching).
Task 4: Check PHP-FPM saturation (slow requests and queueing)
cr0x@server:~$ sudo ss -lntp | grep php-fpm
LISTEN 0 511 127.0.0.1:9000 0.0.0.0:* users:(("php-fpm8.2",pid=1123,fd=8))
What it means: PHP-FPM is listening. That’s not enough; you need to know if workers are busy.
Decision: Enable PHP-FPM status page and inspect active/idle/queue. If you see a queue, you’re CPU-bound or under-provisioned in workers—caching and code reduction beat “more workers” most of the time.
Task 5: Verify PHP opcache is enabled and healthy
cr0x@server:~$ php -i | egrep -i "opcache.enable|opcache.memory_consumption|opcache.interned_strings_buffer|opcache.max_accelerated_files"
opcache.enable => On => On
opcache.memory_consumption => 128 => 128
opcache.interned_strings_buffer => 16 => 16
opcache.max_accelerated_files => 10000 => 10000
What it means: Opcode cache is on, with moderate settings.
Decision: If opcache is off, turn it on. If memory is too small, you’ll churn and lose the benefit—raise it. This is “boring performance,” which is the best kind.
Task 6: Identify slow MySQL queries
cr0x@server:~$ sudo tail -n 20 /var/log/mysql/mysql-slow.log
# Query_time: 2.314 Lock_time: 0.000 Rows_sent: 10 Rows_examined: 512340
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
LEFT JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id)
WHERE wp_posts.post_type = 'product' AND wp_posts.post_status = 'publish'
ORDER BY wp_posts.post_date DESC LIMIT 0, 10;
What it means: A query took 2.3 seconds and examined half a million rows. Mobile feels that as “tap… wait… nothing.”
Decision: Remove/replace the plugin pattern causing this, add the right indexes (carefully), and rely on page caching so you don’t execute it per user.
Task 7: See if the database is I/O bound
cr0x@server:~$ iostat -xz 1 5
avg-cpu: %user %nice %system %iowait %steal %idle
18.22 0.00 3.10 24.88 0.00 53.80
Device r/s w/s rkB/s wkB/s await aqu-sz %util
nvme0n1 220.0 180.0 8200.0 5400.0 18.40 3.20 92.00
What it means: Disk utilization is high, with non-trivial await time; iowait is elevated. Storage is hot.
Decision: If MySQL is on the same box and you see this, you need fewer reads (caching), better indexes, faster storage, or separation of roles. “Add more PHP workers” won’t fix a saturated disk.
Task 8: Check CPU and memory pressure during a slow period
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 1 0 31200 84200 612000 0 0 820 610 420 980 25 6 50 19 0
3 2 0 29800 83500 605000 0 0 1120 900 470 1100 31 7 41 21 0
What it means: Run queue is up, blocked processes exist, and iowait is significant (wa ~ 20%). No swap activity, so this is not swapping.
Decision: This supports an I/O bottleneck or a database bottleneck. Prioritize caching and database/storage tuning over front-end changes.
Task 9: Count third-party domains (latency tax on mobile)
cr0x@server:~$ curl -s https://example.com/ | grep -Eo 'https?://[^"]+' | awk -F/ '{print $3}' | sort -u | head
cdn.example.net
fonts.googleapis.com
fonts.gstatic.com
stats.vendor-a.com
tagmanager.vendor-b.com
What it means: Each third-party domain adds DNS+TLS+request overhead and can block rendering if used poorly.
Decision: If you have more than a handful of third parties on the critical path, start deleting or deferring. On mobile, “but it’s async” is not a guarantee; main-thread work still happens.
Task 10: Find the heaviest images and whether you’re serving modern formats
cr0x@server:~$ find /var/www/html/wp-content/uploads -type f -name "*.jpg" -o -name "*.png" | xargs -r du -h | sort -hr | head
5.8M /var/www/html/wp-content/uploads/2025/12/hero-homepage.jpg
4.1M /var/www/html/wp-content/uploads/2025/12/header.png
3.9M /var/www/html/wp-content/uploads/2025/11/banner-sale.jpg
What it means: Multi-megabyte images are common and brutal on mobile, especially as LCP elements.
Decision: Convert hero images to WebP/AVIF (where supported), resize to realistic dimensions, and ensure responsive srcset is used. If the theme disables it, fix the theme or replace it.
Task 11: Confirm gzip/brotli compression for text assets
cr0x@server:~$ curl -sI -H "Accept-Encoding: br,gzip" https://example.com/wp-content/themes/site/style.css | egrep -i "content-encoding|content-length|vary"
content-encoding: br
vary: Accept-Encoding
What it means: Brotli is enabled. Good.
Decision: If you see no Content-Encoding, enable gzip/brotli. It’s low-risk and helps mobile significantly for CSS/JS/HTML.
Task 12: Check HTTP cacheability of static assets
cr0x@server:~$ curl -sI https://example.com/wp-content/uploads/2025/12/hero-homepage.jpg | egrep -i "cache-control|expires|etag|last-modified"
cache-control: public, max-age=31536000, immutable
etag: "a1b2c3d4"
What it means: Great: long-lived caching. Static assets should be aggressively cached; use file versioning for updates.
Decision: If you see no-cache on images/CSS/JS, fix your server/CDN rules. Otherwise, every mobile visit pays the same download cost again.
Task 13: Locate WordPress cron pressure (surprise background work)
cr0x@server:~$ wp cron event list --due-now --path=/var/www/html
+--------------------------+---------------------+---------------------+------------+
| hook | next_run_gmt | next_run_relative | recurrence |
+--------------------------+---------------------+---------------------+------------+
| wc_scheduled_sales | 2025-12-27 10:12:00 | 1 minute ago | hourly |
| action_scheduler_run_queue | 2025-12-27 10:10:00 | 3 minutes ago | every_minute |
+--------------------------+---------------------+---------------------+------------+
What it means: Scheduled events are due. If WP-Cron runs on user requests, it can spike latency unpredictably.
Decision: Disable WP-Cron triggering on page loads and run a real system cron hitting wp cron event run --due-now on a schedule.
Task 14: Check PHP error logs for “performance by exception”
cr0x@server:~$ sudo tail -n 20 /var/log/php8.2-fpm.log
[27-Dec-2025 10:11:19] WARNING: [pool www] child 2245, script '/var/www/html/index.php' (request: "GET /") executing too slow (3.214 sec), logging
[27-Dec-2025 10:11:19] NOTICE: [pool www] child 2245 stopped for tracing, pid 2245
What it means: You have slow PHP execution, not just slow networks. Something inside WordPress is taking seconds.
Decision: Add application-level profiling (even temporarily), identify the plugin/theme function consuming time, and fix or remove it. Don’t just “increase timeouts.”
What to optimize first (ordered, opinionated)
This is the part people want to skip to. Fine. Here’s the order that wins most often in production.
1) Get a real page cache for anonymous users
If your site serves the same homepage to thousands of anonymous visitors but generates it with PHP+MySQL every time, you’re burning CPU and adding latency for no reason. Page caching (at the edge, reverse proxy, or WordPress plugin) is the single biggest lever for TTFB.
- Edge HTML caching is best when you can do it safely (purging on updates, bypassing for logged-in/cart/checkout pages).
- Nginx FastCGI cache is excellent if you control the server.
- Plugin caching is better than nothing, but plugins can be fragile and inconsistent under load.
Rule: if your public pages aren’t getting cache HITs, fix that before you touch minification.
2) Fix TTFB variability: cron, cache bypass, and backend contention
Users don’t complain about average. They complain about spikes. Mobile amplifies spikes because the network is already slow; add backend jitter and you get “it hangs.”
- Move WP-Cron off page loads.
- Reduce cache bypass rules (query strings, cookies, UTM parameters).
- Ensure your cache key is sane: mobile/desktop should generally share HTML unless you’re truly serving different markup.
- Stop making remote API calls in the request path. Cache them or move to background jobs.
3) Optimize the LCP element (usually a hero image)
On mobile, the page “feels loaded” when the main content appears. That’s LCP. If your LCP is a 5MB JPEG scaled down by CSS, your Core Web Vitals will look like a ransom note.
- Resize to the actual rendered size (plus a little for retina).
- Serve WebP/AVIF with JPEG fallback.
- Don’t lazy-load the LCP image.
- Preload it if you must, but only the right one (avoid preloading desktop assets for mobile).
4) Delete third-party scripts until the site behaves
This is where you get political. Third-party scripts are performance debt you pay forever, in every geography, on every device.
- Remove what you can. Replace what you must. Self-host where reasonable.
- Defer non-critical scripts. Ensure they don’t block rendering.
- Watch for “tag manager sprawl”: one container becomes five vendors, then twenty tags, then sadness.
5) Reduce JavaScript and DOM complexity (themes/page builders)
Page builders make fast prototyping easy and fast rendering hard. If your mobile device spends seconds in scripting and layout, you can compress images all day and still lose.
- Disable unused blocks/widgets/modules.
- Stop shipping sliders, animations, and mega-menus to users who just want to read.
- Choose a theme that respects performance budgets.
6) Make the server boring: modern PHP, opcache, sane FPM, fast storage
Once caching and payload are handled, you still want a stable origin:
- PHP 8.1+ with opcache enabled.
- PHP-FPM configured for your CPU/RAM (no giant process counts that thrash).
- MySQL tuned for your dataset, with slow query logging on.
- SSD/NVMe storage, not shared spinning disks masquerading as “cloud.”
Joke #2: If you “optimized” by installing five performance plugins, you didn’t optimize—you started a small, argumentative committee.
Three corporate mini-stories from the performance trenches
1) Incident caused by a wrong assumption: “Mobile is cached by default”
A mid-size company had a WordPress marketing site plus a blog that fed leads to sales. Desktop looked fine, and the team proudly announced they’d enabled caching. Mobile conversions, however, were mysteriously dropping, and support tickets described the site as “sticky” and “randomly slow.”
The wrong assumption was subtle: their CDN cache rules varied by user-agent, because someone had added a “mobile optimization” years earlier that served slightly different HTML. The CDN treated mobile as a separate cache bucket but didn’t store it long due to a conservative TTL. Worse: a marketing plugin set a cookie for anonymous users, and the CDN was configured to bypass cache whenever any cookie existed.
Desktop users tended to arrive via direct visits and had a warmed cache in their corporate networks; mobile users arrived via social links with tracking parameters and were far more likely to trigger cache bypass. The origin servers were perfectly capable of serving cached desktop content. They were not capable of rendering every mobile page dynamically at peak campaign traffic.
The fix was not “add more servers.” It was to normalize the cache key: ignore harmless cookies, strip/normalize common tracking query strings at the edge, and stop varying HTML by user-agent unless absolutely necessary. They also introduced explicit cache status headers so anyone could see HIT/MISS without guessing.
Result: TTFB became stable. The “randomly slow” reports vanished because the randomness was a caching policy, not a mystery.
2) Optimization that backfired: “Let’s minify and combine everything”
A different organization had a WordPress + WooCommerce setup. Mobile pages were heavy, so the team enabled aggressive optimization: combine CSS, combine JS, defer everything, inline “critical CSS,” and a dozen other toggles that sounded like productivity.
For a week, the lab numbers improved. Then the customer complaints started: buttons sometimes didn’t work, checkout validation failed intermittently, and some product pages had layout glitches only on certain Android devices. The marketing department called it “a brand issue,” which is corporate for “we are escalating this loudly.”
What happened: the optimization plugin changed script order and introduced race conditions. WooCommerce extensions expected certain libraries to exist in a specific order; combining and deferring broke those assumptions. Meanwhile, HTTP/2 meant that combining assets into giant bundles wasn’t even the big win it used to be. They traded reliability for theoretical speed.
The rollback restored functionality, but they didn’t abandon performance work. They adopted a narrower approach: don’t combine unless you’ve proven request overhead is the bottleneck; instead, remove unused scripts, delay only non-interactive third-party tags, and keep the checkout path sacred—minimal transformations, maximal predictability.
Lesson: if a speed tweak can break revenue flow, it should be treated like a deploy with a canary, monitoring, and a rollback plan. “It’s just front-end” is how you end up on a conference call.
3) Boring but correct practice that saved the day: cache observability and capacity baselines
A global company ran multiple WordPress properties and had a recurring problem: every big campaign caused performance “surprises.” The infrastructure team didn’t want heroics; they wanted predictability.
They added two boring things. First: explicit cache instrumentation. Every response carried headers showing edge cache status, origin cache status, and whether the request hit PHP. Second: capacity baselines. They kept a simple dashboard: origin CPU, disk I/O, PHP-FPM queue depth, MySQL latency, and CDN HIT ratio.
During the next campaign, they saw the HIT ratio dip for mobile traffic only. Not guesswork—data. It turned out a newly added A/B testing tool set a cookie on first visit, which bypassed edge caching. They adjusted the CDN to ignore that cookie for non-personalized pages, and the HIT ratio recovered within minutes.
Nothing clever. No new platform. Just visibility and a baseline that made “normal” measurable. The campaign succeeded, and the postmortem was blissfully short because the answer was in the headers.
Common mistakes: symptom → root cause → fix
These are the patterns I see repeatedly: the same symptoms, the same bad guesses, the same fixes that actually work.
1) Symptom: Mobile feels “stuck” before anything appears
Root cause: High TTFB due to cache misses or dynamic rendering on every request.
Fix: Implement full-page caching for anonymous traffic; verify HITs with headers; normalize query strings and cookies that cause bypass.
2) Symptom: First visit is awful, repeat visits are fine
Root cause: No CDN for static assets, or short TTLs, or no immutable caching; mobile network cost dominates first load.
Fix: Long-lived caching headers for static assets, CDN offload, enable brotli/gzip, ensure file versioning.
3) Symptom: LCP is terrible; everything else loads eventually
Root cause: Hero image too big or delayed by lazy-load or render-blocking CSS/fonts.
Fix: Optimize the LCP image (resize + modern format); don’t lazy-load it; preload only the correct variant; trim render-blocking CSS.
4) Symptom: Tapping menu/cart/search feels laggy
Root cause: Main thread blocked by heavy JavaScript and third-party scripts; poor INP.
Fix: Remove unused scripts, defer non-critical tags, avoid giant page builder DOM, audit third parties and delete aggressively.
5) Symptom: Performance fine off-hours, terrible during campaigns
Root cause: Origin saturation (CPU, DB, disk), PHP-FPM queueing, cache HIT ratio collapse.
Fix: Make caching resilient, add capacity, isolate database, reduce dynamic work, and monitor queue depth and I/O. Don’t “scale” without reducing per-request cost.
6) Symptom: Mobile slower than desktop specifically
Root cause: User-agent variation, mobile-specific markup, different cache key, or mobile served larger images due to broken responsive rules.
Fix: Unify HTML between devices where possible; ensure srcset works; confirm CDN caching doesn’t split unnecessarily by device.
7) Symptom: “We installed a cache plugin but nothing improved”
Root cause: Cache isn’t serving (cookie bypass, logged-in traffic, query strings, or plugin conflicts), or the bottleneck is third-party JS and images not HTML generation.
Fix: Verify with headers and logs; if HTML cache is HIT but LCP is still bad, shift to LCP element and JS/third-party cleanup.
8) Symptom: Admin is slow; front-end is slow sometimes
Root cause: Database bloat, autoloaded options explosion, or expensive plugins running everywhere.
Fix: Audit plugins, clean up autoloaded options, reduce background tasks, add object caching only if you understand eviction and memory.
Checklists / step-by-step plan
Here’s a pragmatic plan you can execute without turning your site into a science project.
Phase 1: Establish baseline (same day)
- Measure TTFB and total time from your own server network path (
curlTask 1) and from at least one external location. - Record cache headers and cache status (Task 2). Screenshot the evidence. Don’t trust memory.
- Identify LCP element on key templates (homepage, category, product/article). If it’s an image, note its file and size (Task 10).
- List third-party domains present on the page (Task 9).
- Check origin resource health during typical traffic (Tasks 7 and 8).
Phase 2: Fix the first bottleneck (1–3 days)
- If TTFB is high:
- Implement full-page cache for anonymous users (edge or origin).
- Normalize cache bypass: ignore marketing params; handle cookies carefully.
- Move WP-Cron off requests (Task 13).
- Turn on opcache and confirm it’s working (Task 5).
- If LCP is high but TTFB is ok:
- Resize and re-encode the LCP image; ensure it is not lazy-loaded.
- Ensure static assets have long cache lifetimes (Task 12).
- Enable brotli/gzip (Task 11).
- If interaction is laggy:
- Delete or defer third-party scripts.
- Remove unused theme modules and page builder widgets.
- Avoid “combine everything” toggles unless proven safe on revenue flows.
Phase 3: Stabilize and prevent regression (ongoing)
- Add cache observability: return cache status headers; log upstream response time; track HIT ratio.
- Keep slow query logging on, with rotation.
- Adopt a performance budget: max LCP image size, max third-party domains, max JS KB for mobile templates.
- Test checkout and login flows with every performance change (canary and rollback).
FAQ
1) Why is my WordPress site only slow on mobile, not desktop?
Desktop hides problems with more CPU and often lower latency. Mobile amplifies latency and main-thread work. If TTFB is high, mobile networks make it feel worse; if JS is heavy, mobile CPUs make it feel worse.
2) What should I check first: images or hosting?
Check TTFB first. If TTFB is consistently high, it’s server/caching/DB/edge. If TTFB is fine, fix LCP images and JS. Don’t buy a bigger server to solve a 5MB hero image.
3) Do I need a CDN for mobile performance?
If your audience is geographically distributed or your origin is slow, yes. Even locally, a CDN helps by caching static assets and sometimes HTML. But a CDN won’t fix uncacheable pages or broken cache rules.
4) Is a caching plugin enough?
Sometimes. But plugins can be bypassed by cookies, query strings, logged-in states, and WooCommerce behavior. If you can, prefer caching at the edge or reverse proxy where you can prove HIT/MISS and control bypass rules explicitly.
5) Should I combine CSS/JS files to reduce requests?
Not as a default. With HTTP/2/HTTP/3, fewer requests is less critical than smaller payloads and less JS execution. Combining can backfire by breaking script order and making caching less efficient.
6) My Core Web Vitals show poor INP on mobile. What’s the first move?
Reduce main-thread work: delete third-party scripts, cut down theme/page builder JS, and avoid heavy UI widgets. Then look for specific long tasks in performance tooling, but the “delete first” step usually wins.
7) WooCommerce is slow on mobile—what’s different?
WooCommerce pages often can’t be cached the same way as a blog due to carts, sessions, and personalization. You must be strict about what is cacheable (product pages often are) and keep checkout/cart stable and lightly optimized rather than heavily transformed.
8) Can database indexing alone fix mobile slowness?
It can fix high TTFB when slow queries dominate and caching isn’t viable (or cache is missing). But most public WordPress pages should not be executing heavy queries per view. Use caching so you don’t need heroically optimized queries for anonymous traffic.
9) Why do my performance results vary a lot between tests?
Cache warmth, network variability, and background tasks (cron, cache purges, deployments) cause swings. Intermittent TTFB often points to cache misses, PHP-FPM queueing, or database I/O contention.
Practical next steps
Do these in order, and stop when the metric moves. Performance is a queue of bottlenecks, not a single dragon.
- Measure TTFB with curl and decide: backend first or front-end first.
- Prove caching is working with headers and logs; fix cache bypass rules that kill mobile.
- Optimize the LCP element (almost always an image) and ensure it’s not lazy-loaded.
- Delete third-party scripts until the site behaves on a mid-range phone.
- Stabilize the origin: opcache on, WP-Cron off requests, slow query log on, watch I/O and queue depth.
- Lock in guardrails: performance budget, monitoring, and a rollback plan for “optimizations.”
If you want one mantra: optimize what users feel first. On mobile, that’s usually cacheability and the LCP element. Everything else is detail work—valuable detail work, but only after you stop the bleeding.