DEV Community

Ajeet Singh
Ajeet Singh

Posted on

Off-Main-Thread Architecture: Let the Main Thread Breathe

Modern web applications demand more from browsers than ever before. Analytics, personalization, A/B testing, chat widgets, and countless third-party scripts compete for the same precious resource: the main thread. When this thread gets overwhelmed, users experience jank, delayed interactions, and frustration.

Off-Main-Thread (OMT) Architecture is a design pattern that moves non-UI work away from the main thread, allowing the browser to stay responsive while still executing heavy JavaScript operations.


Table of Contents

  1. Understanding the Main Thread Problem
  2. Web Workers: The Foundation
  3. How Off-Main-Thread Architecture Works
  4. Partytown: OMT Made Practical
  5. Advantages of OMT Architecture
  6. Complexities and Challenges
  7. Production Usage
  8. Things to Keep in Mind
  9. Further Reading

Understanding the Main Thread Problem

The browser's main thread is responsible for:

  • Parsing HTML and CSS
  • Executing JavaScript
  • Calculating layouts and styles
  • Painting pixels to the screen
  • Handling user interactions (clicks, scrolls, typing)

When you add heavy JavaScript execution to this list, something has to wait. That "something" is usually user interactionβ€”causing the dreaded jank.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        MAIN THREAD TIMELINE                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Parse   β”‚   JS    β”‚  Layout β”‚  Paint  β”‚   JS    β”‚  Layout β”‚  Paint β”‚
β”‚  HTML    β”‚ Execute β”‚  Calc   β”‚  Screen β”‚ Execute β”‚  Calc   β”‚  Screenβ”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚          β”‚         β”‚         β”‚         β”‚         β”‚         β”‚        β”‚
β”‚          β”‚  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  β”‚         β”‚        β”‚
β”‚          β”‚     Long Task (>50ms) - UI Blocked!   β”‚         β”‚        β”‚
β”‚          β”‚  User clicks here... waits... waits..β”‚         β”‚        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

The 50ms Rule

According to the RAIL performance model, any task taking longer than 50 milliseconds is considered a "long task" and risks making the UI feel sluggish. Third-party scripts like Google Analytics, Facebook Pixel, or chat widgets can easily exceed this threshold.


Web Workers: The Foundation

Web Workers are the browser's mechanism for running JavaScript in background threads, separate from the main thread.

Types of Workers

Worker Type Purpose DOM Access Scope
Dedicated Worker General computation ❌ None Single page
Shared Worker Shared state across tabs ❌ None Multiple pages (same origin)
Service Worker Network proxy, offline ❌ None Origin-wide

Basic Web Worker Example

main.js (Main Thread)

// Create a new worker
const worker = new Worker('worker.js');

// Send data to the worker
worker.postMessage({ numbers: [1, 2, 3, 4, 5], operation: 'sum' });

// Receive results from the worker
worker.onmessage = (event) => {
  console.log('Result:', event.data); // Result: 15
};

// Handle errors
worker.onerror = (error) => {
  console.error('Worker error:', error.message);
};
Enter fullscreen mode Exit fullscreen mode

worker.js (Worker Thread)

self.onmessage = (event) => {
  const { numbers, operation } = event.data;

  let result;
  if (operation === 'sum') {
    result = numbers.reduce((a, b) => a + b, 0);
  }

  // Send result back to main thread
  self.postMessage(result);
};
Enter fullscreen mode Exit fullscreen mode

Key Limitations of Web Workers

  1. No DOM Access: Workers cannot directly read or modify the DOM
  2. No window Object: Limited browser APIs available
  3. Communication Overhead: Data must be serialized via postMessage()
  4. Separate Context: No shared memory (unless using SharedArrayBuffer)

How Off-Main-Thread Architecture Works

OMT architecture uses workers strategically to keep heavy operations away from the main thread while still allowing those operations to interact with the DOM when necessary.

The Communication Pattern

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         OFF-MAIN-THREAD ARCHITECTURE                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    MAIN THREAD                                      WORKER THREAD
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     β”‚                        β”‚                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚     postMessage()      β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚     DOM       β”‚  β”‚  ◄──────────────────── β”‚  β”‚  Heavy JS     β”‚  β”‚
β”‚  β”‚   Rendering   β”‚  β”‚                        β”‚  β”‚  Execution    β”‚  β”‚
β”‚  β”‚   Layout      β”‚  β”‚  ────────────────────► β”‚  β”‚  (Analytics,  β”‚  β”‚
β”‚  β”‚   Paint       β”‚  β”‚     postMessage()      β”‚  β”‚   Parsing,    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚                        β”‚  β”‚   Compute)    β”‚  β”‚
β”‚                     β”‚                        β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚                        β”‚                     β”‚
β”‚  β”‚ User Input    β”‚  β”‚   Proxy Layer for      β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Event Loop    β”‚  β”‚   DOM Operations       β”‚  β”‚  Virtual DOM  β”‚  β”‚
β”‚  β”‚ (clicks,      β”‚  β”‚  ◄──────────────────── β”‚  β”‚  Proxy        β”‚  β”‚
β”‚  β”‚  scroll,      β”‚  β”‚  ────────────────────► β”‚  β”‚  Interface    β”‚  β”‚
β”‚  β”‚  typing)      β”‚  β”‚   Results/Data         β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚                        β”‚                     β”‚
β”‚                     β”‚                        β”‚                     β”‚
β”‚  🟒 Stays Fast!     β”‚                        β”‚  πŸ”„ Does Heavy Work β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

The Flow

  1. Initialize: Main thread creates a worker and loads heavy scripts into it
  2. Proxy Setup: A proxy layer intercepts DOM API calls in the worker
  3. Message Passing: DOM operations become serialized messages
  4. Main Thread Execution: Only actual DOM mutations run on main thread
  5. Response: Results are sent back to the worker
  6. UI Remains Responsive: Main thread is free for rendering and input

Partytown: OMT Made Practical

Partytown by Builder.io is a library that makes off-main-thread architecture accessible. It runs third-party scripts inside a web worker while proxying DOM access.

How Partytown Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          PARTYTOWN ARCHITECTURE                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    MAIN THREAD      β”‚                        β”‚      WEB WORKER             β”‚
β”‚                     β”‚                        β”‚                             β”‚
β”‚  1. Load Partytown  β”‚                        β”‚  2. Scripts loaded here     β”‚
β”‚     runtime         β”‚ ─────────────────────► β”‚     (GA, GTM, FB Pixel)     β”‚
β”‚                     β”‚                        β”‚                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚                        β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Real DOM      β”‚  β”‚  3. Proxy calls        β”‚  β”‚ Virtual DOM Proxy     β”‚  β”‚
β”‚  β”‚               β”‚  β”‚ ◄───────────────────── β”‚  β”‚                       β”‚  β”‚
β”‚  β”‚ document      β”‚  β”‚  "read document.cookie"β”‚  β”‚ document.cookie       β”‚  β”‚
β”‚  β”‚ window        β”‚  β”‚                        β”‚  β”‚ window.location       β”‚  β”‚
β”‚  β”‚ localStorage  β”‚  β”‚  4. Real values        β”‚  β”‚ localStorage          β”‚  β”‚
β”‚  β”‚               β”‚  β”‚ ─────────────────────► β”‚  β”‚                       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  "session_id=abc123"   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                     β”‚                        β”‚                             β”‚
β”‚  5. Only minimal    β”‚                        β”‚  Script thinks it has       β”‚
β”‚     DOM work here   β”‚                        β”‚  normal DOM access          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Basic Setup

1. Install Partytown

npm install @builder.io/partytown
Enter fullscreen mode Exit fullscreen mode

2. Copy library files to your public folder

npx partytown copylib public/~partytown
Enter fullscreen mode Exit fullscreen mode

3. Add to your HTML

<!DOCTYPE html>
<html>
<head>
  <!-- Partytown configuration (optional) -->
  <script>
    partytown = {
      forward: ['dataLayer.push'],  // Forward these calls to worker
      debug: true                    // Enable debug mode
    };
  </script>

  <!-- Load Partytown runtime -->
  <script src="/~partytown/partytown.js"></script>

  <!-- Third-party scripts with type="text/partytown" -->
  <script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
  <script type="text/partytown">
    window.dataLayer = window.dataLayer || [];
    function gtag(){ dataLayer.push(arguments); }
    gtag('js', new Date());
    gtag('config', 'GA_ID');
  </script>
</head>
<body>
  <!-- Your app content -->
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Framework Integration

Next.js

// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
import { Partytown } from '@builder.io/partytown/react';

export default function Document() {
  return (
    <Html>
      <Head>
        <Partytown forward={['dataLayer.push']} />
        <script
          type="text/partytown"
          src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
Enter fullscreen mode Exit fullscreen mode

React (Vite/CRA)

// index.html or App component
import { Partytown } from '@builder.io/partytown/react';

function App() {
  return (
    <>
      <Partytown forward={['dataLayer.push', 'fbq']} />
      {/* Your app */}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Under the Hood: The Proxy Magic

Partytown uses several clever techniques:

// Simplified view of how Partytown proxies work
// In the worker, Partytown creates proxy objects:

const documentProxy = new Proxy({}, {
  get(target, prop) {
    if (prop === 'cookie') {
      // Synchronously request from main thread
      return syncRequest('document.cookie');
    }
    // ... handle other properties
  },
  set(target, prop, value) {
    if (prop === 'cookie') {
      // Send to main thread
      postToMain({ type: 'set', path: 'document.cookie', value });
    }
    return true;
  }
});

// Scripts in the worker see this proxy as the real document
Enter fullscreen mode Exit fullscreen mode

Synchronous Access Challenge

Web Workers are asynchronous by nature, but many third-party scripts expect synchronous DOM access. Partytown solves this using:

  1. Atomics and SharedArrayBuffer (when available)
  2. Synchronous XMLHttpRequest (fallback)
  3. Service Worker intermediary (another fallback)

Advantages of OMT Architecture

1. Improved Core Web Vitals

Metric Impact
First Input Delay (FID) Reduced blocking = faster response to first interaction
Interaction to Next Paint (INP) Less main thread contention = smoother interactions
Total Blocking Time (TBT) Heavy scripts don't contribute to blocking time
Largest Contentful Paint (LCP) Main thread free to render content faster

2. Better User Experience

BEFORE OMT:
User clicks β†’ [Wait 200ms for JS] β†’ Response
                    ↑
              Main thread busy
              with analytics

AFTER OMT:
User clicks β†’ [5ms] β†’ Response
                ↑
          Main thread free
          Analytics in worker
Enter fullscreen mode Exit fullscreen mode

3. Isolation and Stability

  • Crash Isolation: A misbehaving script in a worker won't freeze the page
  • Memory Isolation: Worker memory issues don't affect main thread
  • CPU Isolation: Worker can use dedicated CPU time

4. Clearer Performance Budgets

// You can now categorize scripts
const mainThreadScripts = [
  'app-bundle.js',      // Core app logic
  'critical-ui.js',     // UI interactions
];

const workerScripts = [
  'analytics.js',       // Google Analytics
  'tracking.js',        // Marketing pixels  
  'chat-widget.js',     // Customer support
  'personalization.js', // A/B testing
];
Enter fullscreen mode Exit fullscreen mode

5. Measurable Improvements

Real-world results from Partytown users:

  • Up to 99% reduction in third-party script main thread time
  • 10-50ms reduction in Total Blocking Time
  • Significant FID improvements on script-heavy marketing pages

Complexities and Challenges

1. Communication Overhead

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    MESSAGE PASSING OVERHEAD                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Without OMT:
document.cookie (1 operation) β†’ 0.01ms

With OMT:
document.cookie (1 operation):
  Worker: Serialize request         β†’  0.1ms
  Worker β†’ Main: postMessage        β†’  0.5ms
  Main: Execute                     β†’  0.01ms
  Main β†’ Worker: postMessage        β†’  0.5ms
  Worker: Deserialize response      β†’  0.1ms
                              Total:   ~1.2ms

For chatty scripts with many DOM calls, this adds up!
Enter fullscreen mode Exit fullscreen mode

2. Synchronous API Challenges

Many browser APIs are synchronous but workers are async:

// This works in main thread:
const width = element.offsetWidth;  // Immediate value

// In a worker, this needs special handling:
const width = await getFromMainThread('element.offsetWidth');
// Or Partytown's sync mechanism via Atomics
Enter fullscreen mode Exit fullscreen mode

3. Limited Browser API Access

Workers don't have access to:

  • DOM (document, Element, etc.)
  • window (partially available as self)
  • localStorage / sessionStorage (directly)
  • Certain APIs like alert(), confirm(), prompt()

4. Debugging Complexity

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    DEBUGGING CHALLENGES                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Traditional:
Console β†’ Script β†’ Error β†’ Stack trace β†’ Line number

With OMT:
Console β†’ Worker proxy β†’ Actual script β†’ Error β†’ 
  ↓
Proxied stack trace β†’ Virtual line number β†’ πŸ€” Where?
Enter fullscreen mode Exit fullscreen mode

DevTools improvements needed:

  • Trace across thread boundaries
  • Map proxied calls to original scripts
  • Monitor message passing performance

5. Script Compatibility

Not all scripts work well in workers:

Script Type OMT Compatibility
Analytics (GA, GTM) βœ… Excellent
Chat widgets ⚠️ May need config
A/B testing βœ… Usually good
Payment SDKs ❌ Often problematic
UI libraries ❌ Not suitable
Heavy DOM manipulation ❌ Not suitable

6. State Synchronization

// Challenge: Keeping state in sync
// Main thread state:
window.userLoggedIn = true;

// Worker sees stale state:
if (window.userLoggedIn) {  // Might be outdated!
  trackLoggedInUser();
}

// Solution: Forward state changes
partytown = {
  forward: ['dataLayer.push', 'updateUserState']
};
Enter fullscreen mode Exit fullscreen mode

Production Usage

Companies Using OMT Architecture

Company Use Case
Builder.io Creator of Partytown, uses it extensively
Shopify Hydrogen framework supports Partytown
Netlify Edge functions + Partytown for marketing sites
Various e-commerce Marketing-heavy sites with many third-party scripts

Real Production Examples

E-commerce Site

Before Partytown:
- Total Blocking Time: 850ms
- Third-party script time: 600ms
- FID: 180ms

After Partytown:
- Total Blocking Time: 280ms  (-67%)
- Third-party script time: ~0ms on main thread
- FID: 45ms (-75%)
Enter fullscreen mode Exit fullscreen mode

Marketing Landing Page

Scripts moved to worker:
- Google Analytics
- Google Tag Manager
- Facebook Pixel
- Hotjar
- Intercom

Result:
- TTI improved by 40%
- Conversion rate increased 12%
Enter fullscreen mode Exit fullscreen mode

Browser Support

Browser Support
Chrome 80+ βœ… Full support
Firefox 79+ βœ… Full support
Safari 15+ βœ… Full support
Edge 80+ βœ… Full support
IE 11 ❌ No worker support

Things to Keep in Mind

1. Start with the Right Candidates

βœ… GOOD CANDIDATES:
β”œβ”€β”€ Analytics scripts (GA, Adobe Analytics)
β”œβ”€β”€ Tag managers (GTM, Tealium)
β”œβ”€β”€ Marketing pixels (Facebook, LinkedIn, Twitter)
β”œβ”€β”€ Heatmap tools (Hotjar, FullStory)
β”œβ”€β”€ Chat widgets (Intercom, Drift)
└── A/B testing (Optimizely, VWO)

❌ POOR CANDIDATES:
β”œβ”€β”€ Payment processors (Stripe.js, PayPal)
β”œβ”€β”€ Authentication SDKs
β”œβ”€β”€ Core UI libraries
β”œβ”€β”€ Real-time collaboration tools
└── Anything requiring immediate DOM feedback
Enter fullscreen mode Exit fullscreen mode

2. Measure Before and After

// Set up performance monitoring
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log('Long Task:', entry.duration, 'ms');
      // Send to analytics
    }
  }
});
observer.observe({ entryTypes: ['longtask'] });

// Track Core Web Vitals
import { onFID, onINP, onTBT } from 'web-vitals';

onFID(console.log);
onINP(console.log);
Enter fullscreen mode Exit fullscreen mode

3. Implement Gradually

Phase 1: Single Script
β”œβ”€β”€ Move Google Analytics to Partytown
β”œβ”€β”€ Monitor for 1-2 weeks
β”œβ”€β”€ Check: Events still tracking?
└── Check: Performance improved?

Phase 2: Expand
β”œβ”€β”€ Add GTM
β”œβ”€β”€ Add marketing pixels
β”œβ”€β”€ Monitor each addition
└── Document any shims needed

Phase 3: Optimize
β”œβ”€β”€ Fine-tune forwarding config
β”œβ”€β”€ Add error monitoring for workers
└── Create runbook for issues
Enter fullscreen mode Exit fullscreen mode

4. Handle Errors Properly

// Configure error handling
partytown = {
  forward: ['dataLayer.push'],

  // Catch errors from worker
  mainWindowAccessors: ['onerror'],

  // Log worker errors
  resolveUrl: (url) => {
    console.log('Partytown loading:', url);
    return url;
  }
};

// Add global error handler for worker issues
window.addEventListener('error', (event) => {
  if (event.filename?.includes('partytown')) {
    // Log to your error tracking service
    trackError('Partytown Error', event.message);
  }
});
Enter fullscreen mode Exit fullscreen mode

5. Have a Rollback Plan

<!-- Feature flag approach -->
<script>
  const usePartytown = window.FEATURE_FLAGS?.partytown ?? true;

  if (usePartytown) {
    // Load Partytown version
    document.write('<script src="/~partytown/partytown.js"><\/script>');
  }
</script>

<!-- Scripts that can fallback -->
<script 
  type="text/partytown"
  data-fallback-type="text/javascript"
  src="https://analytics.example.com/script.js"
></script>
Enter fullscreen mode Exit fullscreen mode

6. Test Third-Party Script Functionality

// Create a test suite for moved scripts
describe('Analytics in Partytown', () => {
  it('should track page views', async () => {
    // Trigger page view
    gtag('event', 'page_view');

    // Verify it was received (check your analytics dashboard or mock)
    await waitFor(() => {
      expect(analyticsReceived('page_view')).toBe(true);
    });
  });

  it('should track custom events', async () => {
    gtag('event', 'button_click', { button_id: 'cta' });

    await waitFor(() => {
      expect(analyticsReceived('button_click')).toBe(true);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Alternative Approaches

While Partytown is popular, other OMT solutions exist:

1. Manual Web Workers

// For computation-heavy tasks
const computeWorker = new Worker('compute.js');

// Offload data processing
computeWorker.postMessage({ 
  action: 'processLargeDataset',
  data: massiveArray 
});
Enter fullscreen mode Exit fullscreen mode

2. Comlink (by Google)

// Simplifies worker communication
import * as Comlink from 'comlink';

// worker.js
const api = {
  async processData(data) {
    return heavyComputation(data);
  }
};
Comlink.expose(api);

// main.js
const worker = new Worker('worker.js');
const api = Comlink.wrap(worker);
const result = await api.processData(largeData);
Enter fullscreen mode Exit fullscreen mode

3. Workerize

// Automatically moves functions to workers
import workerize from 'workerize';

const worker = workerize(`
  export function expensiveCalculation(n) {
    // Heavy computation
    return result;
  }
`);

const result = await worker.expensiveCalculation(1000000);
Enter fullscreen mode Exit fullscreen mode

Further Reading

Official Documentation

Articles & Deep Dives

Video Resources

Tools & Libraries

Performance Monitoring


Conclusion

Off-Main-Thread Architecture represents a fundamental shift in how we think about web performance. Instead of fighting for main thread time, we acknowledge it as a scarce resource and protect it for what matters most: rendering and responding to users.

Key Takeaways:

  1. The main thread is precious - Reserve it for UI work
  2. Web Workers are mature - Excellent browser support
  3. Partytown makes OMT practical - No need to rewrite scripts
  4. Measure everything - Use Core Web Vitals as your guide
  5. Start small - Move one script, validate, expand
  6. Have a rollback plan - Things can go wrong

The web is getting heavier, but our users' patience isn't increasing. Off-main-thread architecture gives us a way to have our analytics cake and eat our performance too.

Top comments (0)