Web Development · Performance
Modern Web Development Best Practices
The decisions behind fast, accessible, maintainable web apps in 2026: Core Web Vitals thresholds, where the performance wins are, and what to test.
Anurag Verma
5 min read
Sponsored
Most “best practices” lists age badly because they name tools. This one tries to name the decisions instead, and points at the deeper write-ups where the detail lives. If you read one section, make it the first.
Performance first
Web performance in 2026 comes down to three measured thresholds and four levers. The thresholds are Google’s Core Web Vitals: LCP under 2.5 seconds, INP under 200 milliseconds, CLS under 0.1. The levers, in order of effect per hour spent, are image optimisation, caching headers, JavaScript volume, and prefetching the next navigation.
Core Web Vitals
Google measures three metrics. FID (First Input Delay) was retired in March 2024 and replaced by INP (Interaction to Next Paint). If your monitoring or your documentation still references FID, update it; search engines no longer use that signal.
- LCP (Largest Contentful Paint), under 2.5 seconds. How long the main visible content takes to render.
- INP (Interaction to Next Paint), under 200 milliseconds. The responsiveness of every interaction, not just the first.
- CLS (Cumulative Layout Shift), under 0.1. How much the layout moves unexpectedly during load.
INP is meaningfully harder than FID was. FID measured one interaction, the first; INP measures all of them and reports near the worst. That change is why sites that passed comfortably under FID started failing without anything getting slower. Heavy JavaScript on the main thread, large React re-renders, and third-party scripts are the usual causes, in roughly that order.
The measurement that matters is field data, not a Lighthouse score on your laptop. Lighthouse runs a simulated load on a fast machine; Chrome User Experience Report data reflects real devices on real networks. A green Lighthouse score with failing CrUX data is common and the CrUX number is the one Google uses.
Where the wins actually are
In rough order of effect per hour spent:
- Images. Correct dimensions, modern formats, and lazy-loading below the fold. This is usually the single biggest LCP lever and the least interesting work, which is why it gets skipped.
- Caching. Content-addressed filenames with a long
max-age,no-cacheon HTML. Getting this wrong is why users still see the old build after a deploy. See HTTP caching in practice. - JavaScript volume. Code splitting, then removing what you can. Every kilobyte is parse and execute time on a mid-range phone, and mid-range phones are most of the web.
- Prefetching the next navigation. The Speculation Rules API makes the next page load before the click, without an SPA rewrite.
Accessibility
Accessibility is not a checklist you run at the end. Retrofitting it is substantially more expensive than building with it, because the fixes land in markup structure and interaction design rather than in styling.
The four that catch the most real problems:
- Semantic HTML before ARIA. A
<button>is focusable, keyboard-operable and announced correctly with no attributes. A<div>withrole="button"needs a tabindex, a key handler, and still behaves subtly differently. - Contrast that holds in both themes. CSS now computes this for you; see contrast-color() is Baseline.
- Keyboard navigation end to end. Tab through the whole page. If focus disappears into a modal you cannot escape, that is a blocker, not a polish item.
- Test with an actual screen reader at least once per project. Automated tools catch perhaps a third of real issues.
The full standard is in the WCAG compliance guide, and WCAG-EM 2.0 now covers mobile and desktop apps rather than only websites.
Modern CSS
A significant amount of what used to need JavaScript or a preprocessor is now platform CSS:
/* Custom properties: theming without a build step */
:root {
--primary: #171717;
}
/* Grid: responsive layout without media queries */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Container queries: components respond to their own space, not the viewport */
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
Container queries are the change worth internalising. Media queries ask how wide the screen is, which is the wrong question for a component that appears in a sidebar on one page and full width on another. Container queries ask how much room the component actually has.
Beyond those,
@scope gives style isolation
without BEM or a build step, and
anchor positioning
replaces a positioning library for tooltips and dropdowns. What is safe to ship
today is tracked in
Baseline 2026.
TypeScript
Type safety pays for itself at the boundaries: API responses, form input, anything crossing a module you did not write. Inside a small pure function it mostly restates what the code already says.
The two settings that decide whether TypeScript is doing real work are strict
and noUncheckedIndexedAccess. Without them you get autocomplete and not much
else. See the
TypeScript 7.0 migration playbook
for what the current compiler changes.
Testing
Test at the level where failure is expensive:
- Unit tests for logic with branches, especially anything involving money, dates or permissions.
- Integration tests for the handful of flows whose breakage would be an incident. Signup, checkout, auth.
- Visual regression for design systems, where a CSS change in one component silently affects thirty.
Coverage percentage is a weak signal, because it measures whether a line ran, not whether a test would notice it breaking. Mutation testing measures the second thing, which is the one you actually care about.
The short version
Ship the images optimised, get the caching headers right, build with semantic
HTML, turn on strict, and test the flows that would page someone. Everything
else on this page is refinement on top of those five.
Frequently asked questions
- What replaced FID, and does it change what I optimise?
- INP replaced it in March 2024, and yes it changes the work substantially. FID measured input delay on the first interaction only, so a page could be sluggish throughout and still pass. INP measures every interaction and reports close to the worst one, which means long tasks anywhere in the session now count. The practical consequence is that main-thread JavaScript, large re-renders and third-party scripts matter for the whole session, not just at load.
- Why does my Lighthouse score disagree with Search Console?
- Because they measure different things. Lighthouse runs a simulated load on your machine under conditions it chooses; Search Console reports Chrome User Experience Report data, which is real visits on real devices and networks. Your laptop on office Wi-Fi is not representative of a mid-range phone on cellular. Use Lighthouse to find problems while developing and CrUX to know whether you actually have one, because CrUX is the data Google ranks on.
- Where should I start if the site is already slow?
- Images, almost always. Correct dimensions so the browser is not scaling a 4000px file into a 400px slot, a modern format, and lazy loading below the fold. It is usually the largest single LCP improvement available and it requires no architectural change. Caching headers come second, because getting those wrong is why users still see the old build after a deploy. JavaScript volume is third and the most work.
- Is ARIA a substitute for semantic HTML?
- No, and reaching for it first is the common mistake. A native button is focusable, operable by keyboard and space bar, and announced correctly by screen readers with no attributes at all. Recreating that with a div and role="button" needs tabindex, key handlers, and still diverges in behaviour from what assistive technology expects. ARIA exists to describe things HTML cannot express, not to re-describe things it already does.
- Is high test coverage a good goal?
- It is a weak one, because coverage only records that a line executed during a test run. It says nothing about whether any assertion would fail if that line were wrong. You can hit 100% coverage with tests that assert nothing. Mutation testing answers the real question by deliberately breaking your code and checking whether the suite notices, which is a much better use of the effort you would spend chasing the last few coverage points.
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored