A website can accumulate JavaScript one reasonable decision at a time. A disclosure needs a handler, a filter needs a request, and a gallery needs a library. Eventually a simple page has a surprising amount of behaviour to initialise before anybody can use it.
The question I find useful is what the page must do before those enhancements are ready. Can someone read the content, follow a link and submit a search? Starting there gives each piece of JavaScript a smaller, clearer job.
Use the controls the browser already provides
A link should usually remain a link. A search can begin with a labelled form and a normal results page. For a straightforward disclosure, HTML’s details and summary elements provide an opening control and an expanded state without a custom click handler.
<details>
<summary>What does the project include?</summary>
<p>Design, implementation and a documented handover.</p>
</details>
This is an illustrative disclosure, not a complete navigation component. Test its label, keyboard behaviour and appearance in the target browsers. More demanding interactions can still need custom code, but the requirement should explain why it is there.
Keep state close to the interaction
For an enhanced article filter, write down the states before writing the animation: idle, loading, results, no results and failure. Decide what happens if somebody selects a second filter before the first request finishes. A smaller component is easier to reason about when it owns that sequence clearly.
The address bar matters too. A filtered view that cannot be bookmarked, refreshed or reached with Back has lost useful browser behaviour. Start from working URLs, then enhance the transition between them. If an update fails, show a clear recovery route rather than leaving the previous results under a new, misleading label.
Keep event handlers and state within the part of the page they control. If a component can be removed or replaced, make its cleanup equally clear. Global listeners that survive every rerender are easy to overlook because the first visit often works perfectly.
Load optional work at the point of need
An editor, map or chart may justify a substantial dependency. It does not follow that every article needs to download it immediately. Code splitting can move optional functionality out of the initial JavaScript payload and load it when the relevant interaction requires it.
That introduces a visible waiting period somewhere else, so design it deliberately. Show a loading state, handle failure and avoid making the first click appear unresponsive. Splitting a file is only helpful if the resulting interaction remains understandable.
A dependency should also have an owner and an upgrade path. If the only reason it remains is that nobody knows which page uses it, record those uses before removing or replacing it. Package size alone cannot answer whether a library is earning its place.
Measure behaviour as well as bytes
Compare transfer size, browser work and the time until the relevant control responds. Use a slower device or throttling, then navigate through the same sequence a visitor would: open, filter, follow a result, return and try again. Small downloads can still contain expensive work or awkward interactions.
Check keyboard operation, focus after an update, loading announcements where needed, and the result with JavaScript unavailable. Those checks expose different failures from a bundle-size report. The aim is a page whose essential actions remain dependable.
Frameworks can be a sensible choice when the application has enough shared state and interaction to justify them. Keeping JavaScript small means controlling the amount of responsibility it takes on. A readable component with a clear boundary is more valuable than saving a few lines by hiding its behaviour.
The same principle applies to the tooling behind the page. A maintainable frontend build process should make those boundaries easier to see and verify.