Load your JavaScript without blocking HTML
For a classic external app bundle, defer is usually the right default. It lets the browser download JavaScript while parsing the document, then runs it after parsing is complete.
<script src="/index.js" defer></script>Why defer instead of async?
- Deferred scripts run after the document has been parsed.
- Multiple deferred scripts execute in document order.
- They still run before DOMContentLoaded, which suits apps that mount after the document is ready.
- Async scripts run as soon as each download finishes, so execution order is unpredictable.
Defer does not make JavaScript execution non-blocking. When the browser starts executing the bundle, that work still occupies the main thread until it yields.
What a visitor may see
This is where Spache helps: on a cache hit, the server can send useful rendered page content immediately, instead of making the browser wait for the app bundle to run before it can build the page. With defer, the browser can parse that HTML and discover its resources while downloading the bundle. That gives the page a chance to show meaningful content sooner, and makes that content available in the HTML that crawlers receive.
The browser may paint the rendered content before the deferred bundle runs, but it is not guaranteed to paint first: a fast download or other browser work can put script execution before the first visible paint.
When the app mounts, it may replace or reconcile the rendered page. Until that work finishes, the content can be visible but not interactive. That is still useful: visitors can start reading real content before the app is ready, rather than staring at an empty page. If mounting replaces the whole body, users may see the rendered content change rather than get an interactive pause before takeover.
Spache can also capture eligible API responses while rendering and inject them into the cached page for the app to replay during startup. When those calls match, the app can get its data without waiting for another round trip to your origin. This often makes the gap between seeing the rendered page and using the app very small, sometimes effectively zero; JavaScript still has to execute, and uncaptured requests still use the network.
Practical recommendations
- Use defer for classic app bundles; avoid async when startup scripts depend on one another or on parsed markup.
- Keep the rendered HTML useful on its own, especially the main content and important links.
- Avoid clearing the whole body at startup. Mount into a dedicated app root and preserve useful rendered content until the app is ready where possible.
- For genuinely interactive content before JavaScript takes over, use progressive enhancement or hydration. Defer alone does not provide that.
Spache makes rendered content available before the visitor’s app bundle executes; it does not turn a client-side app into a hydration-based application. Test with a throttled network and CPU in browser developer tools to see whether your real page paints, changes, and becomes interactive in the order you expect.
How Spache works →