Optimizing Script Loading in Next.js: A Performance-First Approach
Last updated: December 22, 2025
Introduction
There are multiple strategies when it comes to offloading third-party scripts (tag managers, analytics and tracking scripts) and while Next.js has a built-in support for third-party scripts, sometimes you may need a more creative approach to battle the problem when the standard approaches don't cut it.
In this article, we'll explore how to implement delayed script loading in Next.js while considering user interactions and GDPR compliance.
Key Benefits
🚀 Improved Core Web Vitals
⚡️ Faster initial page load
🎯 Better user experience
🔒 GDPR compliance
📊 Accurate analytics tracking
⚠ Important considerations
Make sure to align this approach with marketing team and discuss pros & cons. This approach is not a standard way of loading third-party scripts. If you need to prioritize performance and all other options of third-party script optimization fall short. Our marketing team implemented it successfully for more than a year, but your mileage may vary.
Implementation Steps
1. Create a User Interaction Hook
First, create a hook to detect when users start interacting with your website. This helps delay script loading until the user actually engages with the page.
// useUserStartInteractions.ts
const useUserStartInteractions = (): boolean => {
const [isUserInteractionStarted, setIsUserInteractionStarted] = useState(false);
const listener = useCallback(() => setIsUserInteractionStarted(true), []);
useEffect(() => {
if (isUserInteractionStarted) return;
// Listen for user interactions
window.addEventListener('mousemove', listener, { passive: true, once: true });
window.addEventListener('touchmove', listener, { passive: true, once: true });
// Fallback timeout after 5 seconds
const id = setTimeout(() => {
listener();
// Cleanup listeners
window.removeEventListener('mousemove', listener);
window.removeEventListener('touchmove', listener);
}, 5000);
return () => {
clearTimeout(id);
window.removeEventListener('mousemove', listener);
window.removeEventListener('touchmove', listener);
};
}, [isUserInteractionStarted, listener]);
return isUserInteractionStarted;
};
2. Implement Cookie Policy Management
Advanced: Custom Storage Hook (Optional)
For better state management across components and browser storage synchronization, you can implement a custom storage hook:
// useStorage.ts
function useStorage<T>(
key: string,
initialValue: T,
storage: StorageName = StorageName.LOCAL_STORAGE
): [T, SetValue<T>] {
// Initialize state with value from storage or initial value
const [value, setStoredValue] = useState<T>(getStorageItem(key, storage) || initialValue);
// Create event listener to sync state across tabs/windows
const listener = useCallback(
e => {
const nextValue = getStorageItem(e.type, storage);
setStoredValue(nextValue as T);
},
[storage]
);
// Set up event listeners
useEffect(() => {
window.addEventListener(key, listener, false);
return () => {
window.removeEventListener(key, listener);
};
}, [key, listener]);
// Create setter function
const setValue: SetValue<T> = useCallback(
nextValue => {
const valueToStore = nextValue instanceof Function ? nextValue(value) : nextValue;
setStorageItem({ [key]: valueToStore }, storage);
// Dispatch event to notify other components
window.dispatchEvent(new Event(key));
},
[key, value, storage]
);
return [value, setValue];
}
For GDPR compliance, create a hook to manage cookie consent:
// useCookiesPolicy.tsx
const useCookiesPolicy = () => {
const [cookiesPolicy, setCookiesPolicy] = useStorage(StorageKeys.COOKIES_POLICY, {
allow: false,
timestamp: '',
});
const isExpired = useMemo(() => {
if (!cookiesPolicy.timestamp) return true;
const date = new Date();
const expires = new Date(cookiesPolicy.timestamp);
return date.getTime() > expires.getTime();
}, [cookiesPolicy.timestamp]);
return {
isAllowed: cookiesPolicy.allow,
isExpired,
setCookiesPolicy,
};
};
3. Create a Trackers Provider Component
The main component that manages script loading:
// TrackersProvider.tsx
const TrackersProvider: React.FC<Required<Props>> = memo(({ hubspotPortalId, isGDPRCountry, gtmId }) => {
const isUserInteractionStarted = useUserStartInteractions();
const { isAllowed, isExpired } = useCookiesPolicy();
// Don't render scripts if:
// 1. User hasn't interacted with the page
// 2. User is from GDPR country and hasn't given consent
if (!isUserInteractionStarted || (isGDPRCountry && (!isAllowed || isExpired))) {
return null;
}
return (
<>
{/* Load scripts with afterInteractive strategy */}
{hubspotPortalId && <Script src="/scripts/hubspot.js" strategy="afterInteractive" />}
{gtmId && (
<Script
id="gtm"
strategy="afterInteractive"
// GTM initialization code
/>
)}
{/* Other third-party scripts */}
</>
);
});
Best Practices
Use Next.js Script Component
Utilize the
next/scriptcomponent withstrategy="afterInteractive"for optimal loadingScripts will load after the page becomes interactive
Conditional Loading
Load scripts only after user interaction
Respect user's cookie preferences
Consider geographical location for GDPR compliance
Performance Optimization
Use script loading strategies wisely
Implement timeout fallbacks
Clean up event listeners
GDPR Compliance
Check user's location
Implement cookie consent mechanism
Store user preferences
Implementation Benefits
Performance Metrics
Reduced Initial JavaScript Bundle
Improved First Input Delay (FID)
Better Largest Contentful Paint (LCP)
User Experience
Faster initial page load
Prioritized core content loading
Respectful of user privacy preferences
Conclusion
By implementing delayed script loading with user interaction detection and GDPR compliance, you can significantly improve your Next.js application's performance while maintaining necessary analytics and third-party functionality.
Remember to:
Test performance impact
Monitor Core Web Vitals
Regularly review and update third-party scripts
Keep GDPR compliance up to date
This approach ensures a balance between functionality and performance, resulting in a better user experience and improved SEO metrics.