Twenty-two seconds. That’s how long our Wallet would freeze while decoding transaction data. No scrolling. No clicking. Just a spinning cursor and that sinking feeling that something had gone terribly wrong. This is the story of how we killed that freeze—and in doing so, laid the groundwork for a complete architectural shift in how GRIDNET OS handles browser-side computation.
What you’re about to read isn’t a polished post-mortem written months after the fact. Every line of code described here was written live on stream, bugs and breakthroughs alike, with our community watching in real-time. That’s how we’ve built GRIDNET OS since 2017—transparently, collaboratively, and with the kind of raw problem-solving you don’t usually get to see.
Table of Contents
- 1. A New Paradigm: Decentralized Service-Oriented Architecture
- 2. The Data Journey: From Remote Node to Your Screen
- 3. The Problem: When Binary Decoding Blocks Everything
- 4. Understanding BER: Why We Chose Binary Over JSON
- 5. The Web Worker Solution: Parallel Processing in the Browser
- 6. Implementation Architecture
- 7. Technical Challenges and Solutions
- 8. Performance Impact
- 9. The Road Ahead: Worker-First Architecture
- 10. Conclusion
1. A New Paradigm: Decentralized Service-Oriented Architecture
Before we dive into the guts of BER decoding and Web Workers, let’s zoom out. Way out. Because the performance problem we’re solving isn’t just a JavaScript quirk—it’s a symptom of building something that’s never existed before: the world’s first truly windowed, decentralized operating system.
That phrase gets thrown around a lot in crypto, so let’s be precise about what we mean.
A Brief History of Service-Oriented Architecture
Rewind to the late 90s. Enterprise software was drowning in monoliths—massive codebases where the database layer, business logic, and UI were all tangled together like a bowl of spaghetti that someone left out for too long. Change one thing, break twelve others. Scale one piece, scale everything. It was a nightmare.
The industry’s answer was Service-Oriented Architecture (SOA): break the monolith into independent services that talk to each other through well-defined protocols. SOAP, WSDL, REST—these became the building blocks of modern enterprise software. Different teams could own different services. You could scale the payment processor without touching the user authentication system. Amazon went all-in on this approach internally, and that infrastructure eventually became AWS. Not a bad side effect.
But here’s what SOA never solved: all those services still lived on someone’s servers. Usually a corporation’s. The architecture was distributed, sure, but the trust model? Completely centralized. You were still depending on Amazon, Google, or whoever to not go down, not get hacked, not change their terms of service, not get subpoenaed.
The Missing Piece: Decentralized Trust
Bitcoin showed up in 2009 and solved trustless consensus—but it was a single-purpose ledger. Ethereum added programmability with smart contracts, but its architecture is fundamentally different from SOA. So we asked a different question: what if we took the entire SOA stack—services, protocols, APIs, user interfaces, the works—and made the whole thing decentralized?
GRIDNET OS vs. Ethereum vs. Solana: Architectural Philosophy
To really get why we’re doing things differently, let’s look at what the big players are actually doing under the hood.
Ethereum: The Global Computer
Ethereum’s pitch was brilliant: a world computer where anyone can deploy code that runs exactly as written, forever, without any single party being able to stop it. And it delivered on that promise. But the EVM model comes with trade-offs that become painful when you’re trying to build real applications:
- Everything costs gas. Want to read your own balance? Gas. Query a contract’s state? Gas. Every interaction with the chain has a price tag.
- No sessions, no state. Each transaction is atomic and isolated. There’s no concept of “I’m logged in” or “I’m in the middle of something.”
- UI is your problem. Build your frontend separately. Connect it via web3.js or ethers.js. Host it somewhere. Hope Infura doesn’t go down.
- Every dApp is an island. Each one is a separate website, separate authentication, separate everything.
Solana: The Performance Chain
Solana looked at Ethereum’s throughput bottleneck and said “we can fix that.” And they did—Proof of History, parallel transaction processing, impressive TPS numbers. But architecturally? It’s still the same model with faster execution:
- It’s a runtime, not an OS. Programs are deployed artifacts. The chain executes them. That’s the relationship.
- Speed costs hardware. Validator requirements are substantial. That’s a centralization trade-off whether we like to admit it or not.
- UI is still your problem. Nothing changed there.
- Everything on-chain is permanent. There’s no concept of “let me try this first” or “execute this but don’t record it.”
GRIDNET OS: The Decentralized Operating System
Here’s where we took a different road entirely. We didn’t build a blockchain and then add smart contracts. We built an operating system that happens to be decentralized. That might sound like marketing speak, but the architectural differences are real:
| Aspect | Ethereum/Solana | GRIDNET OS |
|---|---|---|
| Primary Metaphor | Global computer / Blockchain | Operating System |
| User Interface | External websites | Integrated windowed dApps |
| Execution Model | All-or-nothing on-chain | Ephemeral + Committed |
| Read Operations | RPC calls to nodes | Live GridScript threads |
| Developer Experience | Solidity + separate frontend | Full JavaScript API in browser |
| Session State | None (stateless transactions) | Persistent threads with context |
The Ephemeral Execution Model: Ethereum in Your Browser
This is the part that takes a minute to sink in, so bear with us: imagine you could run Ethereum-style code in your browser, using real decentralized infrastructure, but only commit to the blockchain when you actually want to.
Think about what happens in Ethereum when you want to check your balance, preview a swap, or validate a transaction before signing it. You’re either making RPC calls to a centralized provider (hi, Infura) or paying gas for on-chain operations. That’s insane. Why should “let me see my balance” be the same operation as “transfer all my tokens to this address”?
GRIDNET OS has decentralized processing threads. Real execution on real nodes—but ephemeral. Nothing hits the blockchain until you say so:
- Ephemeral Execution: Your browser opens a processing thread. GridScript runs on remote nodes. Results stream back live. But the blockchain doesn’t know and doesn’t care—nothing’s committed yet. You’re using decentralized compute for free-form queries, validations, “what if” scenarios.
- Selective Commitment: Ready to make it real? Transfer some tokens, register a domain, update your data? Now you commit. The network executes with full redundancy, reaches consensus, records it forever.
- Same Infrastructure: And here’s the thing—ephemeral and committed execution use the same nodes, same security model, same everything. The only difference is whether the result gets written to permanent state.
This isn’t a Layer 2 or a sidechain. It’s not “we’ll batch your transactions and settle later.” The ephemeral threads run on the actual GRIDNET Core nodes that maintain the blockchain. Same cryptographic guarantees. Same economic incentives. You just get to choose when permanence matters.
The JavaScript Advantage
All of this is exposed through JavaScript APIs right in your browser. If you can write web apps, you can write decentralized apps:
- Use whatever you already know—React, vanilla JS, CSS frameworks, you name it
- Clean async APIs. No web3 provider dance. No MetaMask popups interrupting your flow.
- Real-time WebSocket streaming from the network—not polling, actual push notifications
- Ephemeral by default, commit when you’re ready
The First Windowed Decentralized OS
Here’s where it gets interesting for users, not just developers. When you use GRIDNET OS, you’re not bouncing between browser tabs, each one some dApp’s website with its own login flow. You’re in an actual windowed environment. Wallet. Block Explorer. Domain Manager. They’re all windows on the same desktop, sharing authentication, talking to each other through system APIs.
This isn’t just a UI choice—it reflects how the system actually works. Applications are first-class citizens here, not websites pretending to be apps. The CVMContext (that’s Virtual Machine Context—you’ll hear a lot about it in this article) is basically the kernel. It manages threads, routes messages between dApps, keeps the network connection alive, and coordinates everything.
Now think about what happens when you open your Wallet. In a normal web app, you’d hit a REST endpoint, get some JSON back, display it. Simple. In GRIDNET OS, your request triggers a cascade across a decentralized network. Remote nodes—real machines owned by real people, economically incentivized to behave honestly—execute your query in GridScript. The results get BER-encoded (a binary format we’ll explain shortly), encrypted with our custom ECC layer (yes, on top of TLS—we’re paranoid like that), and streamed back to your browser. Where it needs to be decrypted, decoded into actual objects, and rendered.
All while keeping the UI responsive.
That last part is where we ran into trouble. And that’s what this article is really about.
What Makes This Different
Traditional applications operate on a simple request-response model with centralized servers. GRIDNET OS operates on a decentralized processing thread model where every operation is cryptographically verified, economically incentivized, and executed across a distributed network of nodes. This creates unique engineering challenges—and unique solutions.
The browser-side subsystem of GRIDNET OS must handle responsibilities that would be unthinkable in traditional web development: decrypting custom ECC-encrypted streams, parsing complex ASN.1/BER-encoded binary data structures, managing real-time WebSocket connections to decentralized nodes, coordinating multiple concurrent “threads” (our virtual threading model), and updating multiple windowed dApps simultaneously—all without freezing the UI.
This is why the Web Worker optimization we’re about to explore matters so much. It’s not just about fixing a UI freeze. It’s about ensuring that the most sophisticated decentralized operating system ever built can deliver the smooth, responsive experience that users expect from modern software—while doing things under the hood that no other platform even attempts.
2. The Data Journey: From Remote Node to Your Screen
Let’s trace what actually happens when you click “Show Balance” in the GRIDNET Wallet. This isn’t theoretical—this is the exact path your data takes, and understanding it explains why we eventually needed Web Workers.
The Complete Data Flow
balance addr
CTransactionDescCSearchResultsCDomainDesc
Execute “balance”
Query blockchain
Compute results
UI Components
Core Processing
Problem Area
Output/Success
The Processing Thread Model
One thing that surprises people: GRIDNET OS has actual threads. Not JavaScript’s fake “threads” (looking at you, event loop), but real processing threads that execute on remote nodes. When your Wallet runs a query, it’s spinning up execution contexts across the network:
- System Thread: Your main execution context. Anything that changes blockchain state—transfers, contract calls, domain registrations—runs here. It’s “committable,” meaning results can be written to the ledger.
- Data Thread: Read-only queries. Balance checks, transaction history, lookups. Runs in parallel with your system thread, so you can query data while formulating a commit.
- Custom Threads: Need to prepare multiple transactions in isolation? Spin up custom threads. They’re sandboxed until you’re ready to merge them.
Crypto-Incentivized Execution
Here’s the magic: every GridScript execution is paid for through ERG (our energy unit). Nodes compete to process your queries because they get compensated. No AWS bill. No central server. Just a global network of machines economically motivated to execute your code honestly and efficiently.
3. The Problem: When Binary Decoding Blocks Everything
So there we were, late 2024, watching the bug reports roll in. “Wallet freezes for 20+ seconds.” “Browser says page unresponsive.” “I thought I crashed it.” The symptoms were brutal: 22 seconds of complete UI death. No scrolling. No clicking. Nothing. Just a frozen screen and a user wondering if they’d lost their funds.
We dug into the profiler and found our villain almost immediately: BER decoding, running synchronously, hogging the main thread like it owned the place.
Why Was It So Slow?
Here’s the thing about JavaScript: it’s single-threaded. One thread. That’s it. Every millisecond you spend parsing binary data is a millisecond you’re not responding to mouse clicks, not animating your loading spinner, not doing literally anything else. The profiler showed us exactly how bad it was:
What makes BER parsing so expensive? It’s doing a lot: recursive descent through nested structures, byte-by-byte tag analysis, length field parsing, dynamic object allocation, BigInt conversions (blockchain amounts get big), string decoding, Uint8Array creation. And unlike JSON—where V8 has years of C++ optimization—our ASN.1/BER parser runs in pure JavaScript. No native fast path. Just raw interpreter overhead.
“Zero frames per second. On a modern MacBook Pro. For 22 seconds straight. We watched it happen live on stream. The chat was not happy.”
4. Understanding BER: Why We Chose Binary Over JSON
“Why don’t you just use JSON like everyone else?” We get this question a lot. And honestly, it’s a fair question. JSON is simple, human-readable, and JavaScript speaks it natively. So why did we choose to wrestle with ASN.1 and BER encoding?
What is BER (Basic Encoding Rules)?
BER is the serialization format for ASN.1 (Abstract Syntax Notation One). If those acronyms mean nothing to you, here’s the context: it’s the same encoding used in TLS certificates, LDAP, SNMP, and basically every serious telecom protocol from the last 40 years. It’s not trendy. It’s not modern. But it’s rock solid, and we chose it for specific reasons:
- Size Matters: BER is compact. Seriously compact. When every byte traverses a decentralized network and costs ERG, you care about payload size. A lot.
- Type Safety: ASN.1 schemas catch type errors at serialization time. That matters when you’re dealing with financial transactions.
- Cryptographic Pedigree: BER has been battle-tested in security-critical systems for decades. Our custom ECC encryption layer already speaks ASN.1—it’s a natural fit.
- Clean Boundaries: GRIDNET Core is C++. The browser is JavaScript. BER gives us a well-specified, language-agnostic interface between them.
The Structure of Our Data
Here’s where it gets hairy. We’re not encoding simple key-value pairs. We’re encoding rich, nested objects—transactions with 21 fields each, domain records, search results containing arrays of transactions. Look at what a single transaction structure contains:
BigInt
address
address
BigInt
BigInt
BigInt
GridScript
BigInt
BigInt
32 bytes
INTEGER (8)
OCTET_STRING (7)
UTF8_STRING (5)
BOOLEAN (1)
Now multiply this by 10, 50, or 100 transactions in a search result, each requiring full parsing, type conversion, and class instantiation. You begin to see why this was computationally devastating.
5. The Web Worker Solution: Parallel Processing in the Browser
The fix, conceptually, was obvious the moment we identified the problem: stop doing expensive work on the main thread. Browsers have had Web Workers since 2010—actual background threads that can run JavaScript without blocking the UI. We’d just never needed them before. Time to learn.
The Key Insight
Here’s the counterintuitive part: the decoding still takes 22 seconds. We didn’t make it faster. We made it invisible. The work happens in the background while the main thread stays butter-smooth at 60fps. User clicks something? Instant response. Scroll? Silky smooth. Animation? Flawless. The heavy lifting happens off-stage where nobody can see it.
Before vs. After
Main Thread Decoding
FROZEN
Web Worker Decoding
Scroll ✓
Animate ✓
INSTANT
"Data ready"→ UI updates seamlessly
6. Implementation Architecture
If moving CPU work to a Web Worker were trivial, everyone would do it. Spoiler: it’s not. We ended up building a three-layer system with some tricky engineering to make everything work seamlessly. Let’s break it down.
The Three-Layer Architecture
Manages Worker lifecycle
Sends ArrayBuffer via
postMessage
Receives plain JavaScript objects
RECONSTRUCTS full class instances
_reconstructTransactionDesc()
new CTransactionDesc(...)
postMessage()
postMessage()
BER DECODING
ASN1.fromBER(buffer)
~95 nested function calls
Create class instances
SERIALIZATION
Extract to plain object
BigInt → string
Uint8Array → Array
Minimal CVMContext implementation
Caches blockchain heights
Enables
SearchResults.instantiate()
Main Thread (Proxy)
Worker (Processor)
Context Stub
Why Serialize and Reconstruct?
Here’s the catch with Web Workers: you can’t just pass objects between threads. The postMessage() API uses something called the Structured Clone Algorithm, which is basically a fancy way of saying “we’ll deep-copy your data, but we’ll strip out anything interesting.” What can’t it handle?
- ✗ Class instances with methods (there go our transaction objects)
- ✗ Functions (obviously)
- ✗ Prototypes (bye bye inheritance)
- ✗ Getters and setters (computed properties? nope)
- ✗ BigInt values (seriously, JavaScript?)
So we can’t just decode BER to CTransactionDesc instances in the Worker and pass them back. The instances would arrive as hollow shells—all data, no methods. Our solution: decode in the Worker, serialize to plain objects, pass those to the main thread, then reconstruct full class instances from the data.
Wait, Isn’t That Double Work?
People asked this on stream, so let’s clarify: we’re not decoding BER twice. The “plain object” isn’t BER—it’s just a regular JavaScript object. The flow is: BER binary → decode → class instance → serialize to plain object → transfer → reconstruct class instance. The expensive BER parsing happens once, in the Worker. The reconstruction on main thread is fast—just wiring up methods to existing data.
Code: Serialization in Worker
// BERDecoderWorker.js - Serialize class instance to plain object function serializeTransactionDesc(instance) { return { _type: 'CTransactionDesc', result: instance.result, value: instance.value.toString(), // BigInt → string sender: instance.sender, receiver: instance.receiver, ERGUsed: instance.ERGUsed.toString(), ERGPrice: instance.ERGPrice.toString(), verifiableID: instance.verifiableID, sacrificedValue: instance.sacrificedValue.toString(), sourceCode: instance.sourceCode, ERGLimit: instance.ERGLimit.toString(), nonce: instance.nonce, confirmedTimestamp: instance.confirmedTimestamp, unconfirmedTimestamp: instance.unconfirmedTimestamp, type: instance.type, tax: instance.tax.toString(), log: instance.log, height: instance.height, keyHeight: instance.keyHeight, parsingResultValid: instance.parsingResultValid, blockID: Array.from(instance.blockID), // Uint8Array → Array sizeInBytes: instance.getSize(), bytecodeVersion: instance.bytecodeVersion, // Cache computed values to avoid recalculation on main thread status: instance.status, confirmations: instance.confirmations, fee: instance.fee ? instance.fee.toString() : '0' }; }
Code: Reconstruction in Proxy
// BERDecoderProxy.js - Reconstruct class instance from plain object _reconstructTransactionDesc(data) { const instance = new CTransactionDesc( data.result, BigInt(data.value), // string → BigInt data.sender, data.receiver, BigInt(data.ERGUsed), BigInt(data.ERGPrice), data.verifiableID, BigInt(data.sacrificedValue), data.sourceCode, BigInt(data.ERGLimit), data.nonce, data.confirmedTimestamp, data.unconfirmedTimestamp, data.type, BigInt(data.tax), data.log, data.height, data.keyHeight, data.parsingResultValid, new Uint8Array(data.blockID), // Array → Uint8Array data.sizeInBytes ); // Use ES6 property setter (not a method!) if (data.bytecodeVersion !== undefined) { instance.bytecodeVersion = data.bytecodeVersion; } // Restore cached computed values instance._cachedStatus = data.status; instance._cachedConfirmations = data.confirmations; instance._cachedFee = BigInt(data.fee); return instance; }
7. Technical Challenges and Solutions
Implemented LIVE on YouTube
What follows isn’t a sanitized “lessons learned” section. This is what actually happened—coded live on stream, debugged in real-time, with the community watching us bang our heads against problems and eventually figure them out. That’s been our process since 2017, and it’s not changing.
These challenges might seem obscure if you’ve never worked with Web Workers. But if you ever try to move complex browser code into a Worker thread, you’ll hit the same walls we did. Consider this a map of the minefield.
Web Workers sound simple: spawn a background thread, do work, send results back. In practice? They’re a separate JavaScript universe with different rules. No DOM. No window object. Strict limitations on data transfer. When you’re trying to make a decade-old codebase work in this alien environment, things break in creative ways.
Here are four problems we hit, and how we solved them. Each one taught us something about how browsers actually work under the hood.
Challenge 1: The Phantom Window — When Your Code Assumes a Browser
Background: Why We Have So Many Library Files
GRIDNET OS’s browser subsystem has evolved over eight years of continuous development. Files like NetMsg.js, tools.js, enums.js, and trieNode.js form the foundation of our browser-side architecture:
- NetMsg.js: Handles network message serialization/deserialization for the VM Meta Data Protocol—the WebSocket-based communication layer between browser and GRIDNET Core nodes.
- tools.js (CTools): A utility class providing cryptographic functions, Base58Check encoding, address validation, and other core operations used throughout the system.
- enums.js: Defines enumeration constants for transaction types, message types, thread states, and other protocol-level values that must match GRIDNET Core’s C++ definitions.
- trieNode.js: Implements Merkle Patricia Trie nodes for blockchain state verification—a fundamental data structure in GRIDNET’s consensus mechanism.
- VMContextPartial.js: Contains partial implementations of CVMContext functionality, split for modularity and code organization.
These libraries were written with a reasonable assumption: they would run in a browser. Browsers have a window object. So code like this was perfectly natural:
// In enums.js - make enums globally accessible Object.entries(enums).forEach(([name, enumObj]) => { window[name] = enumObj; // Attach to global window object }); // In tools.js - check browser capabilities if (window.crypto && window.crypto.subtle) { // Use Web Crypto API }
The Problem: Web Workers Have No Window
Web Workers run in a completely separate JavaScript context. They have self (referring to the worker’s global scope), but no window, no document, no DOM whatsoever. When our BERDecoderWorker tried to import these libraries, they crashed immediately.
The naive solution would be to polyfill window at the top of our worker file:
// BERDecoderWorker.js - This DOESN'T work! globalThis.window = self; // ← Line 1: Executes SECOND import { CTools } from './tools.js'; // ← Line 2: Executes FIRST! // Result: ReferenceError: window is not defined // The import runs before the polyfill!
Why does this happen? ES6 module imports are hoisted. The JavaScript engine processes all import statements before executing any other code in the module. Your polyfill on line 1 actually runs after all imports have been loaded and their top-level code executed.
The Solution: Defensive Coding at the Source
We had to modify the source libraries themselves. Every reference to window needed a guard:
// Modified enums.js - now safe for Workers if (typeof window !== 'undefined') { Object.entries(enums).forEach(([name, enumObj]) => { window[name] = enumObj; }); } // Modified tools.js - check before using window const cryptoAPI = (typeof window !== 'undefined' && window.crypto) ? window.crypto : (typeof self !== 'undefined' && self.crypto) ? self.crypto : null;
Files modified: NetMsg.js, tools.js, VMContextPartial.js, toolsStub.js, trieNode.js, enums.js
This experience reinforced an important lesson: when building systems that may need to run in multiple JavaScript environments (browser main thread, Web Workers, Node.js, service workers), always code defensively against environmental assumptions.
Challenge 2: The Buffer That Wasn’t — ArrayBuffer vs. Uint8Array
Background: How BER Data Arrives
When GRIDNET Core sends a response, the data travels through multiple layers before reaching our decoder:
- GRIDNET Core serializes the response to BER format (C++ layer)
- Data is encrypted with our custom ECC layer
- Encrypted payload travels over WebSocket (within TLS)
- Browser receives it as an ArrayBuffer or Blob
- CConversation (our WebSocket handler) decrypts and extracts BER data
- Data is passed to the decoder, often as a Uint8Array view
Our ASN.1 parsing library (which implements the BER decoding) uses the standard Web API pattern for creating typed array views with offsets:
// Inside ASN1.fromBER() - parsing a nested structure const valueView = new Uint8Array(buffer, startOffset, length);
The Problem: A Subtle Type Mismatch
The Uint8Array(buffer, offset, length) constructor has a critical requirement: the first argument must be an ArrayBuffer, not another Uint8Array. If you pass a Uint8Array, JavaScript doesn’t throw an immediate error—it interprets the arguments differently, treating the “offset” as a length and producing garbage.
When we passed data to the worker, it sometimes arrived as a Uint8Array (depending on how earlier processing had handled it). The result was a cryptic error from deep in the ASN.1 parser:
Error: Invalid BER encoding, offset: -1
The offset of -1 was a telltale sign: the parser was trying to read data from a position that didn’t exist because the buffer had been misinterpreted.
The Solution: Normalize Input Types
We added explicit type checking and conversion at the worker’s entry point:
// In BERDecoderWorker.js - ensure we always have ArrayBuffer function handleDecodeRequest(message) { const { id, type, data, options } = message; // Normalize to ArrayBuffer - critical for ASN.1 library! const bufferData = data instanceof ArrayBuffer ? data : data instanceof Uint8Array ? data.buffer // Extract underlying ArrayBuffer : data; // Now safe to pass to ASN.1 parser const instance = CSearchResults.instantiate(bufferData); // ... }
Lesson learned: When working with binary data in JavaScript, always be explicit about whether you’re dealing with ArrayBuffer (the raw memory) or TypedArrays like Uint8Array (views into that memory). They’re related but not interchangeable in all contexts.
Challenge 3: The Missing Context — When Classes Need Their Environment
Background: What is CVMContext?
CVMContext (Virtual Machine Context) is one of the most important classes in GRIDNET OS’s browser subsystem. It’s the central coordinator that manages:
- WebSocket Connections: Maintains the real-time connection to GRIDNET Core nodes
- Thread Management: Tracks system threads, data threads, and user-defined threads for the decentralized processing model
- Keychain Operations: Coordinates with CKeyChainManager for cryptographic signing and authentication
- Blockchain State: Caches current blockchain height and key height, essential for computing transaction confirmations
- dApp Notifications: Routes events to registered Wallet, Explorer, and other dApp instances
CVMContext follows the Singleton pattern—there’s exactly one instance, accessible via CVMContext.getInstance(). Many classes throughout the system call this method to access shared state.
The Problem: CSearchResults Needs Blockchain Heights
When CSearchResults.instantiate() decodes a list of transactions, each CTransactionDesc needs to calculate its confirmation status. Is this transaction confirmed? How many confirmations does it have? Is it still pending?
These calculations require knowing the current blockchain height. A transaction at height 100,000 has 5 confirmations if the current height is 100,005. The code naturally calls:
// Inside CTransactionDesc status calculation const currentHeight = CVMContext.getInstance().getCachedHeight(false); const confirmations = currentHeight - this.height;
But in the Web Worker, there is no CVMContext! The worker is an isolated environment—it doesn’t have access to the main thread’s singletons, WebSocket connections, or any shared state.
ReferenceError: CVMContext is not defined
at CTransactionDesc.get status [as status]
at CSearchResults.instantiate
The Solution: A Purpose-Built Stub
We created VMContextStub.js—a minimal implementation that provides just enough CVMContext functionality for the worker to operate. It doesn’t manage WebSockets or keychains; it just caches blockchain heights.
// VMContextStub.js - Minimal CVMContext for Worker environment export class CVMContext { constructor() { this.cachedHeight = 100000; // Default, updated per request this.cachedKeyHeight = 10000; // Default, updated per request } getCachedHeight(isKeyHeight = false) { return isKeyHeight ? this.cachedKeyHeight : this.cachedHeight; } // Other stub methods as needed... } let instance = null; export default { getInstance: () => { if (!instance) instance = new CVMContext(); return instance; } };
In the worker, we make this stub globally available and inject the current heights before each decode operation:
// BERDecoderWorker.js - Setup and usage import VMContextStub from '/lib/VMContextStub.js'; // Make CVMContext available globally in worker if (typeof CVMContext === 'undefined') { globalThis.CVMContext = VMContextStub; } function handleDecodeRequest(message) { const { id, type, data, options } = message; // Inject current blockchain heights from main thread if (options?.currentHeight !== undefined) { const ctx = VMContextStub.getInstance(); ctx.cachedHeight = options.currentHeight; ctx.cachedKeyHeight = options.currentKeyHeight; } // Now instantiation works - it can access "CVMContext" const instance = CSearchResults.instantiate(bufferData); // ... }
The main thread’s BERDecoderProxy passes the current heights with each request, ensuring the worker always has accurate data for confirmation calculations. This pattern—injecting context via message parameters—is essential for Worker architectures where shared state doesn’t exist.
Challenge 4: The Race That Nobody Won — Async Timing Gone Wrong
Background: How GridScript Requests Work
When the Wallet executes a GridScript command (like querying balance or searching transactions), a complex flow unfolds:
CVMContext.processGridScriptA()sends the command and creates a pending request with a unique ID- The request is stored in
mPendingRequestsMap, awaiting resolution - GRIDNET Core processes the request and sends back a response via WebSocket
processVMMetaDataMsg()receives the response, which contains both the GridScript result AND BER-encoded metadata (transactions, domain details, etc.)- The metadata is processed and decoded, then the appropriate dApp is notified
- The pending request is resolved and deleted from the Map
The key insight is that a single response often resolves the request through its metadata processing. For example, a domainDetails query returns BER-encoded domain information, which when decoded and processed, triggers notifyNewDomainDetails(), which resolves the original request.
The Problem: Fire-and-Forget Meets Async Processing
Before our Web Worker implementation, BER decoding was synchronous. The code looked like this:
// BEFORE: Synchronous processing processVMMetaDataMsg(msg) { // ... parse message ... // Process metadata (synchronous - blocks until done) this.processBlockchainStatisticsSection(entries, msg); // At this point, request was already resolved by the processing above // This check would correctly find no request to resolve if (this.mPendingRequests.has(reqID)) { this.resolvePendingRequest(reqID, data, 'gridScript'); } }
When we made BER decoding async (via Web Workers), we initially wrote:
// BROKEN: Async processing without await processVMMetaDataMsg(msg) { // ... parse message ... // Fire-and-forget! Returns immediately! this.processBlockchainStatisticsSection(entries, msg).catch(err => { console.error('Error:', err); }); // This runs BEFORE the async processing completes! // Request still exists, so we resolve it here... if (this.mPendingRequests.has(reqID)) { this.resolvePendingRequest(reqID, data, 'gridScript'); // ← Resolves & DELETES } } // Later, when async processing finishes... // notifyNewDomainDetails() tries to resolve the same request // But it's already been deleted!
The symptoms were confusing. Console logs showed:
Resolving request 3 (gridScript) after 127ms ... [20 seconds later] ... No pending request found for ID 3 (domainDetails)
The request was being resolved twice with different type names—first as ‘gridScript’ (prematurely), then the real resolution as ‘domainDetails’ failed because the request no longer existed.
The Solution: Proper Async/Await Flow
The fix required two changes: make the outer function async, and properly await the metadata processing:
// FIXED: Proper async flow async processVMMetaDataMsg(msg) { // ... parse message ... // AWAIT the async processing - don't continue until it's done! await this.processBlockchainStatisticsSection(entries, msg).catch(err => { console.error('[VMContext] Error processing statistics:', err); }); // NOW check - the async processing already resolved the request // So this correctly finds nothing to resolve if (this.mPendingRequests.has(reqID)) { this.resolvePendingRequest(reqID, data, 'gridScript'); } }
The lesson: When converting synchronous code to async, you can’t just add .catch() and move on. You need to trace the entire execution flow and ensure that code depending on the async operation’s completion properly awaits it. Fire-and-forget is rarely what you actually want.
Reflecting on the Journey
These four challenges represent just a sample of what we encountered during the live implementation. Each one taught us something about the intersection of browser APIs, JavaScript’s execution model, and the unique architecture of GRIDNET OS. Solving them on camera, with our community participating in real-time debugging, reinforced why we’ve always believed in transparent, live development.
The code works not because we got it right the first time, but because we worked through each issue methodically, understood the underlying systems, and implemented proper solutions rather than quick hacks.
8. Performance Impact
So did it work? Let’s look at the numbers. Spoiler: yes, it worked. But the details matter, because this wasn’t just about making the freeze go away—it was about doing it without breaking everything else.
Key Metrics
What This Means for Users
- No more “Page Unresponsive” popups. Remember those? Gone. The main thread never blocks long enough to trigger them.
- Actually usable while loading. Click around. Switch tabs. Scroll. Everything works while your transaction history loads in the background.
- Same total time, wildly different experience. The data still takes 22 seconds to process. Users don’t notice because they’re not staring at a frozen screen.
- Scales with blockchain growth. As more transactions hit the chain and search results get bigger, the UI stays smooth. The background thread just works longer.
Zero Breaking Changes
We achieved this transformation without changing any metadata object APIs. CTransactionDesc, CSearchResults, CDomainDesc, CBlockDesc—all retain their original interfaces. Existing dApps (Wallet, Block Explorer, Transaction Monitor, Domain Manager) continue to work without modification.
9. The Road Ahead: Worker-First Architecture
This fix was a wake-up call. We’d been treating Web Workers as a last resort, something to reach for only when absolutely necessary. That’s backwards. Going forward, GRIDNET OS is moving toward a worker-first architecture: if it’s CPU-intensive, it doesn’t belong on the main thread. Period.
What’s Coming Next
- Transferable Objects: Right now we’re cloning ArrayBuffers, which means copying data. We can do zero-copy transfers instead—hand the buffer directly to the Worker. Bigger datasets, same memory footprint.
- Worker Pools: One Worker is nice. Multiple Workers running in parallel across CPU cores? Even better. For batch operations, this could be massive.
- Progressive Decoding: Instead of waiting for all 100 transactions to decode before showing any, stream them to the UI as they’re ready. First transactions appear instantly; the rest trickle in.
- Crypto Offloading: ECC decryption is another CPU hog. Same treatment—move it to a Worker.
- WebAssembly BER Parsing: JavaScript is fast. Native code is faster. We’re looking at compiling the BER parser to WASM for near-C++ performance.
The Vision: Seamless Decentralization
Here’s what we’re aiming for: a decentralized operating system that feels exactly as snappy as Gmail or Google Docs. Not “pretty good for a blockchain app.” Not “acceptable given what it’s doing under the hood.” Just… good. Responsive. Natural. The kind of experience where you forget you’re using something revolutionary.
This Web Worker implementation proves it’s possible. Even operations that take 22 seconds of raw CPU time can feel instant when architected correctly. That’s the standard we’re holding ourselves to.
10. Conclusion
We started with a 22-second UI freeze. We ended with a butter-smooth experience that handles the same data without missing a frame. Along the way, we built a three-layer Worker system, solved four gnarly JavaScript environment problems on live stream, and laid the groundwork for a fundamentally different approach to browser-side architecture.
But here’s the thing: this is just one optimization. One piece of a much larger puzzle. We’re building a decentralized operating system—not a blockchain with smart contracts, not another DeFi platform, but an actual OS with windowed apps, integrated UX, and real-time data streaming. The challenges we face have no precedent because nobody else is building this.
When your data traverses encrypted channels across a global network of incentivized nodes, gets executed in distributed VMs, comes back as binary-encoded structures, and has to be parsed in JavaScript’s single-threaded environment—there’s no Stack Overflow answer for that. Ethereum sidesteps it by keeping UI external. Solana ignores it. We confront it head-on, live on stream, because we believe users deserve both decentralization AND a great experience.
“The best infrastructure is invisible. Users should experience magic, not mechanics.”
What This Article Really Shows
More than a technical fix, this is a window into how we build GRIDNET OS:
- UX is non-negotiable. Doesn’t matter how decentralized you are if users can’t stand using your product. Every architectural choice starts with “how does this affect the person at the keyboard?”
- We build in the open. Eight years of YouTube streams. Every bug, every breakthrough, every “wait, that shouldn’t work but it does” moment. Our community has seen it all.
- The browser isn’t an afterthought. Most blockchain projects treat the web interface as something to bolt on later. For us, the browser-side architecture is core to the OS. It’s not a wrapper—it’s where users live.
- Hard problems require hard choices. BER instead of JSON. Custom ECC encryption. Ephemeral processing threads. These aren’t the easy paths. They’re the right paths for what we’re building.
Technical Summary
- Problem: 22-second UI freeze during wallet unlock due to synchronous BER decoding
- Solution: Offload decoding to Web Workers with serialize/reconstruct pattern
- Result: Zero UI freeze, 60fps maintained, 100% API compatibility preserved
- New Files: BERDecoderWorker.js (361 lines), BERDecoderProxy.js (357 lines), VMContextStub.js (570 lines)
- Modified Files: VMContext.js, NetMsg.js, tools.js, VMContextPartial.js, toolsStub.js, trieNode.js, enums.js
- Metadata Objects Modified: ZERO (TransactionDesc.js, SearchResults.js, BlockDesc.js, DomainDesc.js, IdentityToken.js unchanged)
For Developers: Steal This Playbook
- Profile first. Don’t guess where the bottleneck is. The profiler knows. We wasted zero time on this fix because we knew exactly what was eating CPU.
- Workers are underused. Seriously. If something takes more than 50ms of CPU time, it probably shouldn’t be on the main thread.
- Async is tricky. Race conditions hide in async code. Trace your execution flows. Add awaits where you’re not sure. Debug later if needed.
- Don’t break APIs. If your optimization requires every consumer to change their code, you’ve failed. Internal changes should be invisible externally.
- Think bigger. A single fix is a chance to establish a pattern. We didn’t just fix BER decoding—we built the foundation for moving all heavy computation off the main thread.
The Bigger Picture
Eight years. Thousands of hours of live coding. A community that’s watched this thing grow from an idea to the world’s first windowed decentralized operating system. And we’re nowhere near done.
This article covered one optimization. Important, yes—nobody wants a frozen wallet. But it’s one small piece of something much bigger. We’re asking fundamental questions: What if computing didn’t require trusting corporations? What if you could run applications on decentralized infrastructure as easily as on AWS? What if privacy, ownership, and control weren’t trade-offs against convenience?
GRIDNET OS is our answer. And every fix, every feature, every optimization brings us closer to making that answer something real—something anyone can use.
The GRIDNET Difference
This is what sets GRIDNET OS apart from every other project in the space. We’re not building another blockchain with smart contracts. We’re not building another DeFi platform. We’re building a complete operating system—with windowed applications, integrated user experience, real-time data streaming, and the full power of modern web development—that happens to be decentralized.
The Web Worker implementation is one small piece of that larger vision, but it exemplifies our approach: no compromises on user experience, no shortcuts in engineering, no limits on innovation. We’re building at the edge of what’s technologically possible because that’s where the future lives.
Building the future, one optimization at a time.
The world’s first windowed decentralized operating system.
Watch our development LIVE on YouTube • Continuous development since 2017 • GRIDNET OS


