Most "mobile problems" on websites come from a small set of repeat offenders: oversized images, layout jumps, unreadable text, tiny tap targets and too much JavaScript. The good news is that none of them need a full rewrite. You can fix most of them on an existing site, one change at a time.
This guide walks through twelve fixes, each with code you can adapt. It is ordered roughly from the easiest wins to fix issues to the more involved ones, and ends with how to measure the results so you know the work paid off.
Why mobile deserves its own attention
A phone is not just a small desktop. It usually has a slower CPU, a less stable network, a touch screen instead of a mouse, and a user who is often distracted or in a hurry. A page that feels fine on your development laptop over fast Wi-Fi can feel sluggish on a mid-range phone over a mobile connection.
Google's Core Web Vitals give us three useful measures of that experience:
LCP (Largest Contentful Paint): how quickly the main content appears.
CLS (Cumulative Layout Shift): how much the layout jumps around while loading.
INP (Interaction to Next Paint): how quickly the page responds when someone taps or types.
Every fix below maps to at least one of these, or to plain usability.
- Start with the viewport tag
Without it, mobile browsers render your page at a desktop-width canvas and then shrink it down, which makes everything tiny.
html <meta name="viewport" content="width=device-width, initial-scale=1"> Avoid adding maximum-scale=1 or user-scalable=no. They block pinch-zoom, which is an accessibility problem for people with low vision.
- Serve the right image size to each device
A 2400px-wide image on a 390px-wide phone wastes bandwidth and slows down LCP. Use srcset and sizes so the browser can pick the best file for the screen.
How it works: srcset lists the available files and their real widths. sizes tells the browser how wide the image will be displayed at different viewport sizes. The browser combines that with the device's pixel density and chooses a file.
Where your build pipeline allows it, also offer modern formats such as WebP or AVIF using the element:
Browsers that don't support a format simply fall back to the next option.
- Always set image dimensions to prevent layout shift
The width and height attributes in the examples above let the browser reserve the correct amount of space before the file downloads. Without them, text jumps down when the image finally appears, which is exactly what CLS measures.
Pair the attributes with this CSS so images remain fluid:
css img { max-width: 100%; height: auto; } The browser uses the attributes to work out the aspect ratio, and the CSS lets the image shrink to fit its container. The same idea applies to embedded videos and iframes. The aspect-ratio property is a clean way to reserve space for them:
css .video-embed { aspect-ratio: 16 / 9; width: 100%; } - Lazy-load below the fold, prioritize above it
A common mistake is lazy-loading everything, including the largest image at the top of the page. That delays LCP. Treat the hero or main product image as a priority and lazy-load the rest.
- Use fluid typography instead of a pile of breakpoints
clamp() lets text scale smoothly between a minimum and a maximum size, so you don't need a font-size rule at every breakpoint.
css h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); line-height: 1.15; } body { font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); line-height: 1.6; } Keep body text at around 16px or larger on phones so nobody has to zoom to read it. Using rem units rather than px also respects the user's browser font-size settings.
- Build layouts that adapt without media queries
CSS Grid's auto-fit creates a card layout that reflows on its own as the container changes size:
css .cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); } The min(100%, 260px) part is important. Without it, a column with a 260px minimum can overflow a very narrow screen and cause accidental horizontal scrolling, one of the most common mobile bugs.
For simple one-dimensional layouts, Flexbox with wrapping does a similar job:
css .row { display: flex; flex-wrap: wrap; gap: 1rem; } .row > * { flex: 1 1 240px; } - Use container queries for reusable components
Media queries respond to the viewport. But a card component might sit in a wide main column on one page and a narrow sidebar on another. Container queries let the component respond to the space it actually has.
css .card-wrapper { container-type: inline-size; } .card { display: grid; gap: 0.75rem; } @container (min-width: 480px) { .card { grid-template-columns: 160px 1fr; } } This makes components more portable and reduces the number of one-off overrides in your stylesheet. Container queries are supported in all current major browsers.
- Make tap targets big enough
Small links and buttons are frustrating on touchscreens, and they cause mis-taps. A good rule is at least 44 by 44 CSS pixels. WCAG 2.2 sets 24px as a minimum for a pass, and Apple's and Google's guidelines suggest larger.
css button, .nav a { min-height: 44px; min-width: 44px; padding: 0.75rem 1rem; } Leave some space between neighbouring targets too. A row of tightly packed text links in a footer is a classic problem. Padding the links, instead of only increasing the gap, enlarges the tappable area.
- Tame fonts
Web fonts are a frequent cause of slow first paint and of text that flashes or jumps.
css @font-face { font-family: "Inter"; src: url("/fonts/inter-var.woff2") format("woff2"); font-weight: 100 900; font-display: swap; } html <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin> Use font-display: swap so text appears right away in a fallback font.
Preload only the one or two font files used above the fold.
Serve WOFF2 and limit the weights and character sets to what you actually use.
Consider a variable font if you use several weights, since one file can replace many.
- Control third-party scripts and JavaScript weight
On mobile, JavaScript is often the biggest cost, because it has to be downloaded, parsed and executed on a slower CPU. It is also the main driver of poor INP.
html <script src="/analytics.js" defer></script> <script src="/chat-widget.js" async></script> Practical steps:
Use defer for scripts that need the DOM and async for independent ones.
Audit your tags. Old tracking pixels, unused chat widgets and duplicate analytics are very common.
Split large bundles so each page loads only what it needs.
Break up long tasks. If a click handler does heavy work, yield back to the browser so the interface can update:
js button.addEventListener("click", async () => { showSpinner(); // Let the browser paint before heavy work await new Promise((resolve) => setTimeout(resolve, 0)); doHeavyWork(); }); - Respect user preferences
Responsive design is also about the person using the device, not only the screen size. Two media queries cost almost nothing and help a lot:
css @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } @media (prefers-color-scheme: dark) { :root { --bg: #0f1115; --text: #e8eaed; } } Heavy animations can be uncomfortable for some users and drain battery. A dark theme also helps people who use their phone at night.
- Prevent horizontal scrolling and fix the usual suspects
Sideways scrolling on a phone makes a site feel broken. Common causes and quick fixes:
css /* Long words or URLs breaking the layout */ p, li { overflow-wrap: anywhere; } /* Wide tables */ .table-wrap { overflow-x: auto; } /* Elements wider than the screen */ *, *::before, *::after { box-sizing: border-box; } To find the culprit, open DevTools in responsive mode at 320px width and look for the element that extends past the edge. Avoid hiding the problem with overflow-x: hidden on body as a first resort, since that can mask real layout bugs and break sticky positioning.
Measure before and after
Fixes only matter if you can see their effect. Use a mix of lab and real-world tools:
Lighthouse in Chrome DevTools, with mobile throttling turned on, for a quick lab test.
PageSpeed Insights to see real-user field data (Core Web Vitals) when your site has enough traffic.
Chrome DevTools Performance panel with CPU throttling to find long tasks.
A real mid-range phone on a mobile connection. Emulators hide a lot.
Change one thing at a time and re-test, so you know what helped. Lab scores fluctuate between runs, so compare trends rather than single numbers.
Common mistakes to avoid
Lazy-loading the main hero image.
Using fixed pixel widths on containers.
Hiding content on mobile instead of designing it properly for small screens.
Disabling zoom.
Loading every font weight "just in case".
Testing only at your own device's screen size.
Quick checklist
Viewport meta tag present
Responsive images with srcset, plus width and height
Hero image prioritized, others lazy-loaded
Fluid type with clamp()
Flexible grids with no horizontal scrolling at 320px
Tap targets at least 44px
Fonts preloaded, with font-display: swap
Scripts deferred and audited
Reduced-motion and dark-mode preferences respected
Tested on a real phone
Final thoughts
Good mobile performance rarely comes from one big trick. It comes from many small, sensible decisions: send fewer bytes, reserve space for what's coming, keep the main thread free, and make everything easy to read and tap. Start with the quick wins at the top of this list, measure, and work down.
Which of these has made the biggest difference on your projects? Let me know in the comments.