PyScript in a Web Worker: Keep the UI Responsive While Python Crunches Data
Running Python in a PyScript Web Worker keeps the browser UI smooth. Learn why, how to set it up, and what trade‑offs you’ll face.
14 Mar 2026, 10:36 UTC

The Problem: UI Freezes When Python Runs in the Main Thread
When a <py-script> block contains a CPU‑intensive loop, the browser’s single UI thread is blocked. CSS animations, hover states, and click handlers stop responding until the Python code finishes. The symptom is easy to spot: a spinning wheel or a frozen button during a heavy calculation.
The Solution: Run Python in a Web Worker
A Web Worker runs in its own thread, isolated from the UI. PyScript exposes a worker mode that compiles the same CPython/WebAssembly bundle and executes it inside the worker. The main thread remains free to paint, process events, and keep the page interactive. The worker can’t touch the DOM directly; instead, it sends messages back to the main thread, which then updates the UI.
Setting it Up – Pin the PyScript Version and Configure the Worker
Because the API surface changes between releases, start by pinning a specific version in your index.html. The example below uses PyScript 2026.10.14, the latest stable release at the time of writing.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PyScript Worker Demo</title>
<link rel="stylesheet" href="https://pyscript.net/2026.10.14/pyscript.css"/>
<script defer src="https://pyscript.net/2026.10.14/pyscript.js"></script>
</head>
<body>
<h1>Heavy Loop in a Worker</h1>
<div id="status">Idle…</div>
<button id="startBtn" py-click="start_worker()">Start</button>
<py-config>
packages = ["micropip"]
</py-config>
<!-- Main‑thread handler (runs in the UI thread) -->
<py-script>
import asyncio
from pyscript import worker
async def start_worker():
# Launch the worker script below (see next block)
await worker.run("worker.py")
# Listen for messages from the worker
worker.add_event_listener("message", lambda e: document.getElementById("status").innerText = e.data)
</py-script>
<!-- Worker script – heavy computation runs here -->
<py-script worker>
import time
# Simulate a heavy CPU task
def heavy_loop():
total = 0
for i in range(10_000_000):
total += i
return total
result = heavy_loop()
# Send the result back to the main thread
worker.post_message(f"Result: {result}")
</py-script>
</body>
</html>
Key points:
workerattribute on the second<py-script>tells PyScript to run that block in a Web Worker.- The main‑thread
<py-script>usesworker.run()to start the worker file andworker.add_event_listener()to receive messages. - All DOM updates happen inside the main‑thread handler; the worker never touches the page directly.
A Worked‑Through Example – Heavy Loop + UI Update
When you click the “Start” button, the following sequence occurs:
- The main thread calls
worker.run("worker.py")and the worker starts. - The worker executes
heavy_loop(), which takes a few seconds. - While the worker is busy, the browser continues to paint the button and the page remains responsive.
- After the loop finishes, the worker calls
worker.post_message(), sending the result string. - The main‑thread listener updates the
#statusdiv with the result.
To confirm that the UI stays smooth, you can add a CSS animation to the button:
#startBtn {
transition: transform 0.3s;
}
#startBtn:hover {
transform: scale(1.1);
}
During the heavy loop, the hover effect should still animate smoothly if the worker is correctly isolated.
Trade‑offs & Limitations
- No Direct DOM Access: Workers cannot use
documentorwindow. All UI changes must be marshalled back. - Message Passing Overhead: Large data payloads (e.g., big NumPy arrays) need to be serialized, which can add latency.
- Debugging Complexity: Tracebacks from the worker appear in the worker console, not the main thread console.
- Feature Support: Some PyScript helpers (e.g.,
py-*attributes) are only available in the main thread. - Browser Compatibility: All modern browsers support WebAssembly and Workers, but older or niche browsers may lack full support.
How to Verify It Works
- Network Panel: Confirm that
pyscript.jsand the worker bundle are downloaded only once. - Console Logs:
- Open the main thread console – you should see the button’s click handler and any errors from the main script.
- Open the worker console (via the “Workers” tab in devtools) – you should see the heavy loop running and the
post_messagecall.
- Animation Test: Hover over the button while the worker is busy; the scale transition should still be smooth.
- Check the
#statusdiv after completion; it should display the computed result.
Actionable Takeaways
- Pin your PyScript version to avoid API drift.
- Use
workermode for CPU‑heavy tasks; keep UI logic in the main thread. - Marshal results via
worker.post_message()and handle them withworker.add_event_listener(). - Test with a simple animation to confirm the UI stays responsive.
- Monitor the worker console for tracebacks; add
worker.onerrorhandlers if you need robust error reporting.
By following this pattern you can harness the full power of Python in the browser while keeping the user experience smooth.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.