SharedArrayBuffer
and cross-origin isolationThe JavaScript memory stores every piece of data and instruction used during program development and runtime in binary form. JavaScript, also known as ECMAScript, is a memory-managed language.
The JavaScript engine accesses and manages the memory by itself, and it allocates memory for each program or chunk of code written and executed. It also performs the garbage collection of data that is no longer found in the memory.
Although JavaScript is a memory-managed language, it helps manage data, too. But it has flaws. For example, JavaScript can allocate more than the free space needed in the memory for a particular program or variable. In some cases, garbage collection in JavaScript can be slow.
To give developers the ability to allocate and share data in views (using typed arrays) between multiple threads from a memory, the ArrayBuffer
and SharedArrayBuffer
features were introduced.
SharedArrayBuffer
?When discussing SharedArrayBuffer
, we can easily focus on the physical words: “shared,” “array,” and “buffer.”
An array is a data structure used in programming to store data elements consisting of different data types (strings, booleans, numbers, and objects). A buffer is part of the memory storage used to store data temporarily before it is sent or received for usage.
ArrayBuffer
is an array unlike any other — it is an array of bytes, meaning that only bytes are accepted.
To use shared memory in JavaScript, you need to create the SharedArrayBuffer
. This is done by using the SharedArrayBuffer
object, which creates a new object constructor for writing and sharing data between multiple threads.
SharedArrayBuffer
On January 5, 2018, SharedArrayBuffer
was disabled in all major browsers due to a vulnerability attack that was found in modern CPU architecture.
Since then, SharedArrayBuffer
was re-enabled in Google Chrome v67 and can now be used on platforms that have its site isolation feature enabled, which we’ll cover in a subsequent section of this article. This update protects against the Spectre vulnerability attack and makes your site more secure.
Below, we are going to explore how to share memory using SharedArrayBuffer
in JavaScript. First, we’ll share memory, then update and synchronize it, and finally, debug it.
SharedArrayBuffer
in JavaScriptOne of the perks of using SharedArrayBuffer
is the ability to share memory in JavaScript. In JavaScript, web workers serve as a means of creating threads in JS code.
However, web workers are also used side-by-side with SharedArrayBuffer
, which enables the sharing of raw-binary data between the web workers by pointing directly to the memory where each data has been stored or previously accessed.
Let’s take a look at an example of how to share memory using SharedArrayBuffer
.
Before we begin, create three files: index.html (where we linked the scripts), script.js (main thread), and worker.js (worker thread).
<!--index.html file--> <DOCTYPE html> <html> <head> <title>using shared array buffer</title> <meta charset="UTF-8"> <meta name="sharedArrayBuffer" description="using cross-orgiin-isolation in the web browser"> <script type="text/JavaScript" src="script.js"></script> </head> <body> <h3>Take a look at your browser console :)</h3> <script type="text/JavaScript" src="worker.js"></script> </body> </html>
Let’s look at the main thread (the script.js file) first. Here, we access worker.js, then create a shared memory using the SharedArrayBuffer
object, and set its bytes per length to 1024
(note: any required byte per length can be used).
Using a typed array of type Int16Array
to interpret the data being passed, we assign a number to the typed array (20
)to be shared from the main thread. We send the buffer to the worker thread with the use of postMessage
.
/*MAIN THREAD*/ const newWorker = new Worker('worker.js'); const buffMemLength = new SharedArrayBuffer(1024); //byte length let typedArr = new Int16Array(buffMemLength); //original data typedArr[0] = 20; //sending the buffer to worker newWorker.postMessage(buffMemLength);
In order to share the data from the main thread with the worker thread, we set an eventListener
to run when the data is received. Here, notice that we are using an Int16
typed array to display the data in the browser console.
/*WORKER THREAD*/ addEventListener('message', ({ data }) => { let arr = new Int16Array(data); console.group('[the worker thread]'); console.log('Data received from the main thread: %i', arr[0]); console.groupEnd(); postMessage('Updated'); })
In your browser console, you should see this:
[the worker thread] worker.js:7 Data received from main thread: 20 worker.js:8
Since the addition of SharedArrayBuffer
to JavaScript, updating shared memory has become easier. Using the previous example, we are going to update the data from the worker thread.
Let’s set the original arr[0]
from the main thread to the dataChanged
variable declared above in the scope (in the worker thread).
/*WORKER THREAD*/ let BYTE_PER_LENTH = 5; addEventListener('message', ({ data }) => { var arr = new Int16Array(data); console.group('[worker thread]'); console.log('Data received from main thread: %i', arr[0]); console.groupEnd(); //updating the data from the worker thread let dataChanged = 5 * BYTE_PER_LENTH; arr[0] = dataChanged; //Sending to the main thread postMessage('Updated'); })
In order for the data to be updated from the worker thread, we call an onmessage
event that will fire in the main thread, indicating data was updated from the worker thread.
/*MAIN THREAD*/ const newWorker = new Worker('worker.js'); const buffMemLength = new SharedArrayBuffer(1024); //byte length var typedArr = new Int16Array(buffMemLength); //original data typedArr[0] = 20; //sending the buffer to worker newWorker.postMessage(buffMemLength); //onmessage event newWorker.onmessage = (e) => { console.group('[the main thread]'); console.log('Data updated from the worker thread: %i', typedArr[0]); console.groupEnd(); }
In your browser console, you should see:
[the worker thread] worker.js:7 Data received from main thread: 20 worker.js:8 [the main thread] script.js:15 Data updated from the worker thread: 25 script.js:16
Synchronizing shared memory is important because, when implemented, synchronizing causes the shared memory to run simultaneously without been altered. To incorporate synchronization in shared memory, developers use Atomic operations.
Atomics
ensures each process is executed consecutively before the next, and that all data that’s been read from a memory or written to a particular memory are being executed one after the other with the help of the wait()
and notify()
methods.
SharedArrayBuffer
and cross-origin isolationSince May 2021, there have been several critical updates to shared memory — including cross-origin isolation — in JavaScript that enable developers to debug shared memory more efficiently. It’s currently supported in Firefox 79+ and on desktop Chrome, but updates from Chrome 92 will be accessible to sites with cross-origin isolated pages.
To implement SharedArrayBuffer
, you need a secure environment that limits access using one or more response header directives. This is called cross-origin isolation, and, despite the previously discouraged use of shared memory, it has proven to be a better way to secure your website.
Cross-origin isolation is a new security feature (as of April 2021) that was added to the browser. In short, it is the result of sending two HTTP headers on your top-level document (COOP and COEP). These headers enable your website to gain access to web APIs such as SharedArrayBuffer
and prevent outer attacks (Spectre attacks, cross-origin attacks, and the like).
Previously, websites using shared memory could load cross-origin content without permission. These websites could interact with window pop-ups that are not of the same origins, potentially causing a security breach or a loophole to gain access to user information on the website. For websites using shared memory, it became of great importance to be secure and also protect user information.
Now that we know more about the context around cross-origin isolation, let’s enable it on our website.
First, enable the cross-origin-opener-policy (COOP) header at the top level of your document, with same-origin
:
Cross-Origin-Opener-Policy: same-origin
This header isolates the page from any cross-origin pop-ups in the browser so that they will not be able to access documents or send direct messages to them. It also ensures your page is in a secure context with pages with the same top-level origins.
Next, send a cross-origin-embedder-policy header (COEP) with a value indicating require-CORP
(cross-origin resource policy).
Cross-Origin-Embedder-Policy: require-corp
This ensures all resources loaded from your website have been loaded with CORP. The COEP header breaks every integration that requires communication with cross-origin windows in the browser, such as authentications from third-party servers and payments (checkouts).
By setting these headers at the top level of your documents, your website is now in a secure context and provides access to the use of web APIs.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.