Design

Modern JavaScript Practices for Clean, Maintainable Websites (2026)

By Ken Key Apr 12, 2025 5 min read Updated Oct 16, 2026

Modern JavaScript Practices for Clean, Maintainable Websites (2026)
The short answer

Modern JavaScript practice in 2026 means shipping less code, not more. Use native browser features like form validation, the dialog element, and CSS :has() before adding a library. Load scripts with defer or type="module" so they never block rendering, and keep data flow obvious.

Part of the complete web design guide.

On this page

Most “modern web principles” posts are a list of buzzwords with no code behind them. This one is the opposite. I’ve been building and maintaining sites for years, and the principles that actually hold up in 2026 are boring on purpose: keep the bundle small, keep the data flow obvious, and keep the browser doing the work it’s good at.

Start with less JavaScript, not more

The single biggest performance lever on most sites is deleting code, not adding it. A marketing page that ships 400KB of framework code to render three paragraphs is a self-inflicted wound. Modern browsers handle a lot natively now, and every year that list gets longer.

A few things you can do without a build step:

  • Native form validation with required, pattern, and type="email" instead of a validation library.
  • CSS :has() and container queries instead of JS that measures elements and toggles classes.
  • The <dialog> element for modals, which handles focus trapping and the backdrop for you.
  • Native lazy loading on images with loading="lazy".

None of that is exotic. It’s just the platform catching up to what we used to need jQuery and a plugin for.

Load JavaScript the way the browser expects

Where you put your script tag still matters. If a script doesn’t touch the DOM before the page renders, it should not block parsing.

<!-- blocks rendering, avoid for non-critical scripts -->
<script src="/app.js"></script>

<!-- downloads in parallel, runs after parse -->
<script src="/app.js" defer></script>

<!-- runs the moment it downloads, order not guaranteed -->
<script src="/analytics.js" async></script>

Use defer for anything that touches the DOM on load. Use async for independent scripts like analytics that nothing else depends on. And use ES modules natively so you skip a bundler for small projects:

<script type="module" src="/main.js"></script>

Modules are deferred by default, scoped, and support import without a build tool. For a small site, that’s often the whole toolchain.

Fetch data without the ceremony

The fetch API has been the standard for years, but people still forget the two things that trip everyone up: it doesn’t reject on HTTP errors, and it needs an explicit abort path.

async function getUser(id, signal) {
 const res = await fetch(`/api/users/${id}`, { signal });

 if (!res.ok) {
 throw new Error(`Request failed: ${res.status}`);
 }

 return res.json();
}

// Cancel if the user navigates away or types again
const controller = new AbortController();
getUser(42, controller.signal).catch(err => {
 if (err.name !== 'AbortError') console.error(err);
});

That res.ok check is not optional. A 404 or 500 resolves normally, and your code will happily try to parse an error page as JSON. The abort controller matters on search-as-you-type and any route where the user can leave before the response lands.

Keep state where it belongs

A lot of messy front-end code comes from state living in three places at once. Pick one source of truth and derive everything else from it.

  • URL state for anything shareable: filters, tabs, pagination, search terms. If a user can’t bookmark it or send it to a coworker, it probably belongs in the URL.
  • Server state for data you don’t own. Cache it, don’t copy it into local state and let the two drift.
  • Local state for UI that only matters right now: an open menu, a draft input value.

When I audit a site, “we have a bug where the list doesn’t update” almost always traces back to the same data being stored twice. One copy goes stale. The fix is deleting one of them.

Accessibility is not a separate project

Semantic HTML does most of the accessibility work for free. A <button> is focusable, keyboard-activatable, and announced correctly. A <div onclick> is none of those things, and patching it takes more code than just using the button.

<!-- works everywhere, no JS needed -->
<button type="button" onclick="toggleMenu()">Menu</button>

<!-- needs tabindex, role, keydown handler, and still gets it wrong -->
<div onclick="toggleMenu()">Menu</div>

The same idea applies to images with real alt text, headings in order, and labels tied to inputs. It’s less work than retrofitting it after a complaint.

Test the behavior, not the implementation

Tests that assert on internal function names break every time you refactor. Tests that assert on what a user sees survive rewrites. In 2026 that usually means the built-in test runner in Node plus a browser tool for anything interactive.

import { test } from 'node:test';
import assert from 'node:assert/strict';

test('formats a price in USD', () => {
 assert.equal(formatPrice(1999), '$19.99');
});

Node’s test runner needs no dependencies now. For DOM behavior, a real browser beats a simulated one, because the bugs you’re hunting usually live in layout and event handling that a fake DOM doesn’t reproduce.

The boring principles win

Ship less code. Let the platform do what it already does well. Keep one source of truth for state. Write markup that works before JavaScript loads, so the page still functions when a script fails. None of this is glamorous, but it’s what separates a site that’s still easy to change in two years from one nobody wants to touch.

If you’re weighing a rebuild and want someone who’s actually maintained these systems to look at it, my Long Island web design work covers everything from small business sites to custom apps, and I’m happy to talk through the tradeoffs before you commit to a stack.

FAQ

Do I still need a build tool in 2026?

For a small site, often no. Native ES modules, modern CSS, and the platform’s own features cover a lot. You reach for a bundler when you have many modules, need TypeScript, or want tree-shaking across a large dependency graph.

Is a JavaScript framework always the wrong choice?

No. Frameworks earn their weight on apps with lots of interactive state. The mistake is reaching for one on a mostly static brochure site where the framework costs more than it saves.

What’s the fastest performance win for an existing site?

Measure first, then cut. In practice it’s usually removing render-blocking scripts, deferring what you can, and shrinking or dropping dependencies that ship more than you use.

Have a project like this?

I do Long Island web design for Long Island businesses. If you need this done right, let’s talk.