DEV Community

Yoriiis
Yoriiis

Posted on

Efficient script loading strategy

Loading JavaScript efficiently is critical for any website's performance.

At Prisma Media, we made this transition several years ago, and the solution we use today remains reliable and effective across all our sites.

Our pages must load scripts in a defined order while bundling resources efficiently.

The typical sequence is:

  1. Consent management
  2. User connection
  3. Site resources (e.g. styles, scripts)
  4. Third-party scripts (e.g. Google Tag Manager, analytics)

Before 🦖: a JavaScript loader to control execution

Back in 2018 we relied on scriptjs, a small library that lets you load scripts programmatically and control execution order. We placed our calls to scriptjs in a <script> tag just before </body> and listed each resource in the exact order we needed.

But two problems became clear over time:

  • Performance: page rendering was delayed
  • Maintenance: the library was no longer actively maintained

Those issues pushed us to rethink script loading entirely.


Back to basics

After moving away from a JavaScript loader, our goal was to rely on the most native browser features possible. The reasoning was simple:

  • The closer we stay to native HTML and browser standards, the more aligned we are with long-term best practices
  • We no longer wanted to depend on a third-party library that might require another migration in a few years
  • The solution had to be sustainable and easy to deploy across all our websites

The obvious choice was to use regular <script src="…"> tags with the right attributes.

As explained brilliantly in Flavio Copes' article on async and defer, the best strategy is to place scripts in the <head> with the defer attribute.

This placement enables scripts to download in parallel while the browser continues parsing. It ensures scripts execute in order after parsing and makes the DOM interactive sooner.


Handling our loading sequence

For scripts such as consent management, user connection, or third-party scripts served from a CDN, the transition was straightforward. We simply added <script defer src="…"></script> in the desired order.

The bigger challenge came from our own resources. Our platform is a multi-page application (MPA), not a single-page app (SPA). Our websites are server-rendered with Twig templates and we use Webpack to package all resources (styles, scripts, etc.).

Back in 2018 we had a single entry point for the entire site. That setup generated a large JavaScript file shared across all pages, even though each page had different needs. There was no code splitting or shared-chunk optimization.

When we decided to rely on native <script> tags, we wanted to align with our MPA architecture by producing page-specific bundles instead of a single monolithic file. Generating the right <link> and <script> tags for each page could not be done manually, so we needed a webpack plugin to generate them automatically at build time.

At that time the popular html-webpack-plugin assumed a JavaScript-based template and didn't fit our needs.

To bridge the gap I created chunks-webpack-plugin: a Webpack plugin that analyzes entry point dependencies and generates HTML fragments with the appropriate <link> and <script> tags.

This kept the approach fully native, just standard <script> elements, while enabling per-page bundles with granular chunk splitting. For details on that optimization step you can read Granular chunks and JavaScript modules for faster page loads.


💡 Key notes:

  • Modules: <script type="module"> are deferred by default, the defer attribute is unnecessary
  • Legacy fallback: use nomodule attribute for polyfills or legacy bundles; modern browsers will skip them automatically
  • Attribute precedence: if both async and defer are set, modern browsers give priority to async
  • Inline scripts: async and defer have no effect on inline scripts (those without a src attribute)

HTML rendering

Below is a simplified example of the final markup. Each script follows the execution order described above.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Home</title>
    <script defer src="consent.js"></script>
    <script defer src="user-connection.js"></script>
    <!-- Page-specific bundle generated by Webpack -->
    <script defer src="home.js"></script>
    <script defer src="gtm.js"></script>
  </head>
  <body>
    <!-- Page-specific content goes here -->
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Maintaining efficient and reliable script loading

  • Performance: measurable improvements in First Contentful Paint (FCP) and in Largest Contentful Paint (LCP), thanks to deferred, non-blocking script loading
  • Standards and maintainability: scripts are delivered as standard <script> elements, generated by Webpack, ensuring consistent behavior across all pages
  • Best practices: we consistently favor <script> tags with a src attribute over inline JavaScript blocks. This makes scripts cacheable, easier to debug, and aligned with browser parsing rules

By combining native <script> tags and a controlled build-time generation, we ensure predictable execution, page-specific bundles, and a scalable, reliable strategy across all websites.


Top comments (0)