If you’ve ever pasted a 15MB JSON file into a browser-based formatter, you’ve likely seen it: the screen freezes, typing lags, the browser throws an "Aw, Snap!" error, or the fan spins up like a jet engine.
The culprit isn't JavaScript being slow—it’s blocking the UI thread.
When you run JSON.parse(), complex regex replacements, or type generation on the main thread, you block rendering, user inputs, and animations.
In this article, we’ll look at how we built MyJSONPal—a client-side JSON and data studio—to process multi-megabyte payloads instantly without freezing the browser using React 19, CodeMirror 6, and Web Workers.
The Solution: Off-Main-Thread Processing
To keep the editor buttery smooth, we need to offload heavy computation to a background thread using a Web Worker.
[ Main Thread ] ----> postMessage(JSON) ----> [ Web Worker ]
CodeMirror / UI AST Auto-Fix / Conversions
│ │
└────────── Receive Output Back ────────────────┘
By decoupling computation from rendering:
- The editor remains interactive at 60 FPS while background work happens.
- Heavy processing never blocks typing or scrolling.
- Large payloads are processed in parallel with the UI lifecycle.
Step-by-Step Architecture
1. Define a Unified Processing Contract
To keep workers predictable and type-safe, create a shared contract for all inputs and outputs.
// types.ts
export type TargetFormat =
| 'json'
| 'csv'
| 'yaml'
| 'typescript'
| 'zod'
| 'sql';
export interface TaskInput {
action: 'format' | 'minify' | 'fix' | 'convert';
code: string;
targetFormat?: TargetFormat;
indentation?: number;
}
export interface TaskResult {
success: boolean;
output: string;
error?: {
message: string;
line?: number;
column?: number;
};
metrics?: {
originalBytes: number;
outputBytes: number;
timeMs: number;
};
}
2. Create the Worker Task Processor
Inside worker.ts, listen for incoming messages, execute pure processing logic, and return the result.
// worker.ts
import { processJsonPayload } from './jsonProcessor';
import type { TaskInput } from './types';
self.onmessage = (event: MessageEvent<TaskInput>) => {
const startTime = performance.now();
try {
const result = processJsonPayload(event.data);
const endTime = performance.now();
self.postMessage({
...result,
metrics: {
...result.metrics,
timeMs: Math.round(endTime - startTime),
},
});
} catch (err: any) {
self.postMessage({
success: false,
output: '',
error: {
message: err.message || 'Processing failed',
},
});
}
};
3. Build a Worker Client with Fallback Support
Not every environment handles Web Workers the same way. For example:
- Server-side rendering (SSR)
- Embedded webviews
- Restricted browser environments
Always provide a fallback path.
// workerClient.ts
import type { TaskInput, TaskResult } from './types';
let workerInstance: Worker | null = null;
function getWorker(): Worker | null {
if (typeof window === 'undefined') return null;
if (!workerInstance && window.Worker) {
workerInstance = new Worker(
new URL('./worker.ts', import.meta.url),
{
type: 'module',
}
);
}
return workerInstance;
}
export async function executeTask(
input: TaskInput
): Promise<TaskResult> {
const worker = getWorker();
// Primary path: execute off-main-thread
if (worker) {
return new Promise((resolve) => {
worker.onmessage = (e: MessageEvent<TaskResult>) =>
resolve(e.data);
worker.postMessage(input);
});
}
// Fallback path: lazy-load on main thread
const { processJsonPayload } = await import('./jsonProcessor');
return processJsonPayload(input);
}
4. Connect It to React & CodeMirror 6
Now wire asynchronous processing into your editor using debouncing to avoid flooding worker threads on every keystroke.
// EditorWorkspace.tsx
import React, {
useState,
useEffect,
useCallback,
} from 'react';
import { executeTask } from './workerClient';
export const EditorWorkspace = () => {
const [inputCode, setInputCode] = useState('');
const [outputCode, setOutputCode] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const handleTransform = useCallback(async (code: string) => {
if (!code.trim()) return;
setIsProcessing(true);
const result = await executeTask({
action: 'convert',
code,
targetFormat: 'typescript',
});
if (result.success) {
setOutputCode(result.output);
} else {
console.error(result.error?.message);
}
setIsProcessing(false);
}, []);
useEffect(() => {
const timer = setTimeout(() => {
handleTransform(inputCode);
}, 150);
return () => clearTimeout(timer);
}, [inputCode, handleTransform]);
return (
<div className="grid grid-cols-2 gap-4">
<textarea
value={inputCode}
onChange={(e) => setInputCode(e.target.value)}
placeholder="Paste JSON here..."
/>
<div className="relative">
{isProcessing && (
<div className="absolute top-2 right-2 text-xs">
Processing...
</div>
)}
<pre>{outputCode}</pre>
</div>
</div>
);
};
Bonus: The "Magic" Auto-Repair Pattern
Moving processing into a worker unlocks CPU budget for automatic JSON repair without affecting UI responsiveness.
For example, you can handle:
- Trailing commas
- Single quotes
- JavaScript comments
- Python literals (
True,False,None) - Unquoted object keys
// jsonFixer.ts
export function repairJson(dirtyJson: string): string {
return dirtyJson
// Remove UTF-8 BOM
.replace(/^\uFEFF/, '')
// Replace Python/JS literals
.replace(/:\s*True\b/g, ': true')
.replace(/:\s*False\b/g, ': false')
.replace(/:\s*None\b/g, ': null')
// Remove JS comments
.replace(
/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm,
'$1'
)
// Fix unquoted keys
.replace(
/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g,
'$1"$2":'
)
// Convert single quotes
.replace(
/'([^'\\]*(\\.[^'\\]*)*)'/g,
'"$1"'
)
// Remove trailing commas
.replace(/,\s*([}\]])/g, '$1');
}
Because this logic runs inside a worker, even large malformed files can be repaired without causing frame drops or blocking the UI.
Results & Takeaways
By moving payload processing, AST repair, and type generation into a Web Worker architecture, we achieved three major improvements:
🚀 Zero Thread Blocking
Typing, scrolling, and editor interactions remain smooth at 60 FPS, regardless of JSON file size.
🔒 True Data Privacy
All processing happens entirely in the browser. No payloads are uploaded, stored, or transmitted to remote servers.
⚡ Instant Feedback
Even large datasets can be transformed in under 20ms without freezing the browser.
Try It Yourself
You can try this exact architecture in production with MyJSONPal — a free, privacy-first JSON studio built with:
- Astro
- React 19
- CodeMirror 6
- Tailwind CSS v4
- Web Workers
Final Thoughts
Web Workers aren't just an optimization anymore—they're a requirement for modern browser-based developer tools handling large datasets.
If your users are working with multi-megabyte files, schema generation, AST transforms, or code conversion, moving heavy processing off the main thread is one of the highest-impact performance improvements you can make.
How are you handling heavy processing in your client-side applications? Let me know in the comments 👇
Top comments (1)
Thanks for reading this! Please let me know your suggestions in the comments!☺️