Skip to main content

Optimizing Next.js: Lazy Loading Third-Party Scripts Like Intercom

NR Tech Studio Team
NR Tech Studio Team NR Tech Studio
11 min read

When architecting high-performance web applications using Next.js, one of the most significant performance bottlenecks is the indiscriminate inclusion of third-party scripts. Services like Intercom, while providing essential business functionality, are notorious for injecting heavy JavaScript payloads that block the main thread, increase Total Blocking Time (TBT), and negatively impact Core Web Vitals such as Interaction to Next Paint (INP). It is crucial to understand that simply adding these scripts to a _document.js or layout.tsx file is a fundamental architectural error that ignores the critical rendering path.

Next.js does not magically optimize these scripts for you; it requires a deliberate strategy to defer execution until the browser’s idle state or a specific user interaction occurs. This article details the engineering approach to implementing effective lazy loading for third-party scripts, ensuring that your application maintains high performance without sacrificing the features required for customer engagement. We will explore the technical mechanics of the next/script component, the nuances of different loading strategies, and the underlying browser behavior that governs script execution.

The Anatomy of Third-Party Script Performance Impact

To appreciate why lazy loading is necessary, we must analyze how the browser processes external scripts. When a browser encounters a standard <script> tag, it pauses the HTML parser to fetch, parse, and execute the JavaScript. For large, complex SDKs like Intercom, this process can take several hundred milliseconds, even on fast connections. This delay is compounded by the fact that third-party vendors often load additional dependencies, creating a waterfall of network requests that compete for bandwidth with your application’s critical assets.

From a memory management perspective, these scripts often execute immediately upon injection, consuming heap memory before the user has even interacted with the page. In a Next.js environment, where we strive for hydration speed, this is suboptimal. A script that executes during the hydration phase can delay the moment the page becomes interactive, leading to a poor user experience. The goal is to move these scripts into a background task queue where they do not interfere with the primary execution context.

Consider the difference between standard injection and deferred loading. In a standard setup, the script is part of the initial document request. In a deferred setup, we effectively decouple the script’s lifecycle from the page’s load lifecycle. This requires an understanding of the defer and async attributes, which next/script manages internally. However, simply using these attributes is not enough; we must also consider the timing of execution relative to the user’s intent.

Architecting with next/script

The next/script component is the primary tool for managing third-party scripts in the Next.js ecosystem. It provides a declarative API that simplifies the management of script loading strategies. The key to its power lies in the strategy property, which dictates when the script should be loaded. The four primary strategies are beforeInteractive, afterInteractive, lazyOnload, and worker.

For services like Intercom, afterInteractive is often the default choice, but it is rarely the most performant. afterInteractive ensures the script loads after the page becomes interactive, which is better than standard injection but still competes for resources during the critical post-load period. For truly non-essential services, lazyOnload is the superior choice, as it defers the script until the browser is completely idle. This ensures that the main thread is clear for user interactions and that network resources are prioritized for critical application code.

Implementation requires careful consideration of the script’s dependencies. If your application logic depends on the global variable provided by the script (e.g., window.Intercom), you must ensure that you are checking for its existence before calling any methods. This pattern prevents runtime errors when the script is not yet present in the DOM. Using the onLoad callback provided by next/script is the standard way to initialize the script safely.

Implementation Strategy: The Idle Load Pattern

To implement an effective lazy loading strategy for Intercom, you should avoid loading it on the initial render unless absolutely necessary. A common pattern is to wait for the window.onload event or even a specific user interaction, such as clicking a ‘Contact Us’ button or scrolling to a specific section of the page. This approach ensures that the heavy Intercom SDK is only fetched when the user demonstrates an intent to interact with the chat interface.

Here is a robust implementation pattern using React state to manage the script lifecycle:

import Script from 'next/script';
import { useState } from 'react';

export default function ChatWidget() {
  const [isLoaded, setIsLoaded] = useState(false);

  return (
    <>
      {!isLoaded && (
        <button onClick={() => setIsLoaded(true)}>
          Open Support Chat
        </button>
      )}
      {isLoaded && (
        <Script
          src="https://widget.intercom.io/widget/your-app-id"
          strategy="lazyOnload"
          onLoad={() => console.log('Intercom loaded')}
        />
      )}
    </>
  );
}

This code ensures that the Intercom script is never downloaded until the user explicitly requests it. This significantly reduces the initial bundle size and improves the LCP (Largest Contentful Paint) score. By managing the script state within a component, we also allow for conditional loading based on route or user authentication status, further optimizing resource utilization.

Handling Global Namespace Collisions

When dealing with third-party scripts, global namespace pollution is a significant concern. Most legacy SDKs attach themselves to the window object. In a TypeScript environment, this requires careful type definitions to avoid compilation errors. You should extend the Window interface to include the third-party properties, allowing for type-safe interactions with the SDK.

Furthermore, because lazy-loaded scripts are asynchronous, you must always wrap calls to the third-party SDK in a defensive check. A common mistake is to attempt to call window.Intercom('show') before the script has finished executing. This leads to ‘Intercom is not a function’ errors. Implementing a custom wrapper function that queues calls until the script is fully initialized is a best practice for maintaining application stability.

Example of a safe wrapper:

const safeIntercomCall = (...args) => {
  if (typeof window !== 'undefined' && window.Intercom) {
    window.Intercom(...args);
  } else {
    console.warn('Intercom not yet initialized');
  }
};

This wrapper provides a safety net, ensuring that your application logic does not crash if the third-party script is delayed or fails to load entirely, which is a common failure scenario in unstable network conditions.

Advanced Resource Prioritization and Prefetching

While lazy loading is essential, there are cases where you might want to prefetch a script to ensure it is available when the user eventually interacts with it. This is a delicate balance. You want to avoid blocking the initial page load while ensuring that the transition to the chat widget feels instantaneous. Next.js provides built-in mechanisms for resource prioritization, but for third-party scripts, you are often at the mercy of the vendor’s CDN performance.

One strategy is to use the <link rel="dns-prefetch" /> and <link rel="preconnect" /> tags to establish early connections to the third-party domain. This reduces the latency of the initial handshake when the script is finally requested. By adding these tags to your next/head, you can shave off precious milliseconds from the total script loading time without significantly impacting the initial page load performance.

However, be cautious with over-optimizing. Preconnecting to too many domains can actually degrade performance by consuming the browser’s limited connection pool. Only preconnect to domains that are critical for your application’s functionality. For Intercom, preconnecting to widget.intercom.io and js.intercomcdn.com is usually sufficient to provide a performance boost without causing resource contention.

Common Pitfalls and Debugging Strategies

The most common pitfall in lazy loading third-party scripts is the ‘missing functionality’ issue. Developers often load a script lazily and then discover that certain features, like deep-linking to the chat or triggering specific events on page load, no longer function correctly. This occurs because the DOM elements or the window state the script expects are not available when the script executes.

To debug these issues, use the Network tab in your browser’s Developer Tools to monitor the timing of the script execution. Filter by ‘Script’ and observe when the Intercom bundle is requested. If you see the request happening during the initial page load, your strategy prop is not working as expected. Verify that the next/script component is not being re-rendered unnecessarily, which could trigger multiple script downloads.

Another frequent issue is script bloat. Some SDKs include unnecessary dependencies that you may not need. While you cannot modify the external script directly, you can control how you interact with it. Avoid calling large API methods in the onLoad hook if they involve complex DOM manipulation. Instead, break these calls into smaller, asynchronous tasks using requestIdleCallback or setTimeout to spread the computational load over a longer period.

Comparing Loading Strategies

Choosing the right loading strategy requires understanding the trade-offs between user experience and performance metrics. The following table provides a summary of the available strategies in next/script and their typical use cases:

Strategy Description Use Case
beforeInteractive Executes before the page is hydrated Critical scripts like tag managers or security tools
afterInteractive Executes after the page is hydrated Standard analytics, marketing trackers
lazyOnload Executes during browser idle time Chat widgets, social media embeds
worker Executes in a web worker Complex data processing, heavy SDKs (experimental)

As shown, lazyOnload is the most appropriate strategy for non-critical third-party scripts like Intercom. It ensures that the main thread remains responsive during the initial load, which is critical for maintaining high performance in modern web applications. The worker strategy is an emerging technology that holds promise for offloading even more work from the main thread, though it requires careful implementation due to the complexities of communicating between the worker and the main thread.

System Architecture for Scalable Third-Party Management

In large-scale applications, you should move away from hardcoding next/script components throughout your codebase. Instead, implement a centralized ‘Script Manager’ component that handles the loading logic for all third-party services. This allows you to apply consistent performance policies across the entire application and makes it easier to update or replace vendors without touching dozens of files.

A centralized manager can also act as a proxy for your business logic. For instance, you can define a configuration object that maps feature flags to script loading strategies. If a feature is disabled, the script manager simply does not render the next/script component, ensuring that your users only download the code that is relevant to their session. This is a highly efficient way to manage technical debt and keep your codebase clean.

When scaling, consider the impact of third-party scripts on your overall infrastructure. If you find that you are loading a large number of scripts, you might want to look into ‘Partytown’, a library that offloads third-party scripts to a web worker. While more complex to set up, it provides a significantly cleaner separation of concerns and can dramatically improve the performance of heavily instrumented applications.

Cluster Resources

To further refine your approach to building high-performance applications, it is essential to look at the broader context of your development lifecycle. [Explore our complete Software Development directory for more guides.](/topics/topics-software-development/)

Factors That Affect Development Cost

  • Complexity of existing script integrations
  • Number of third-party dependencies
  • Required level of performance optimization
  • Infrastructure constraints for asset delivery

Implementation effort varies based on the number of scripts and the architectural complexity of the existing application.

Frequently Asked Questions

Does Next.js do lazy loading?

Yes, Next.js provides built-in support for lazy loading through the next/script component and dynamic imports. These features allow you to defer the loading of components and scripts until they are actually needed.

How to optimize third-party JavaScript?

Optimization involves deferring non-essential scripts, using the correct loading strategies, and preconnecting to required domains. You should also audit your scripts regularly to remove unused or redundant dependencies.

What is the difference between afterInteractive and beforeInteractive?

afterInteractive loads the script after the page becomes interactive, which is suitable for most scripts. beforeInteractive loads the script before hydration, which is reserved for critical scripts that must be available immediately.

How to fix blocking third-party scripts?

You can fix blocking scripts by moving them to a lazy loading strategy like lazyOnload or using a web worker library like Partytown to offload the execution from the main thread.

Lazy loading third-party scripts like Intercom in Next.js is not merely a performance optimization; it is a critical requirement for maintaining a professional, high-performing web application. By leveraging the next/script component and adhering to the principle of deferred execution, you can ensure that your application remains responsive and provides a smooth experience for your users. The strategies discussed—from choosing the correct loading strategy to implementing safe global access patterns—form the foundation of a robust performance strategy.

As you continue to refine your application, remember that the goal is to balance the need for third-party functionality with the performance requirements of your users. Always measure the impact of every script you include and be prepared to remove or replace those that do not provide sufficient value. By following these engineering best practices, you ensure that your application remains lean, fast, and scalable, regardless of the third-party integrations you choose to support.

NR Tech Studio builds custom web apps, mobile apps, SaaS platforms, and internal tools for growing businesses. If you’re working through a technical decision, feel free to reach out — no commitment required.

References & Further Reading