WebSocket - persistent across page loads?

, Alexander Goedde

WebSocket connections do not persist across page loads. This post discusses the issue and possible solutions using shared Web workers.

The Issue

WebSocket offers persistent connections and bidirectional communication with a server instead of being limited to HTTPs request/response pattern and strict client-to-server communication. Together with a low per-message overhead, this allows efficient, low-latency communication between the client and the server.

WebSocket connections do not, however, persist across loading of new pages from the same domain, or page reloads. A WebSocket connection only persists as long as the page is still open (not navigated away from).

However, such cross-page persistence would be useful because

  • it would save on the overhead of establishing a new connection while navigating across pages from a single domain

  • it would be useful to reduce the number of connections to a server when multiple tabs have opened the same HTML page, each establishing a separate WebSocket connection normally

  • it might also save employing a mechanism to connect different WebSocket session to a specific user identity on the server, e.g. for chat identity or user tracking

This article discusses shared Web workers, and if and how to use them to solve above issue.


Related Posts

Persistent across reloads?

The work that led to this post was inspired by a question which comes up every now and again on StackOverflow:

Is there a way to make WebSocket connections persist across page loads?

And by the fact that shared Web workers were suggested as possible (future) solutions to this.

In this post, I'll cover

  • what (shared) Web workers are
  • use cases for persistent WebSocket connections
  • browser support
  • a test case for shared Web workers and WebSocket
  • which use cases are covered by this
  • what the spec has to say on the topic
  • a suggestion to help cover more use cases

What are web workers?

Web workers are one of the newer features or HTML5 and allow JavaScript to be executed in a separate thread from the page main thread that runs the page UI. The main thread and the worker communicate via messages. The existence of a Web worker is directly dependent on that of the page which instantiated it.

architecture diagram for using a shared web worker to handle websocket connection to the server

Shared Web workers allow the same worker thread to be used by multiple pages from the same domain.

Note: The same origin policy applies to shared Web workers. Means: Web workers can only be "shared" when they come from the same origin.

Shared Web workers are not reliant on any particular page for their existence, as new pages can be added and old ones removed. It is this aspect that inspires the idea that they may possibly persist across page loads - even if there is only a single page from a particular domain open.

Use cases for persistent WebSocket connections

So, as mentioned initially, the first possible use case would be to have a WebSocket connection which persists across the loading of new pages from the same domain, e.g. when surfing a Web site.

This would save on all the overhead of creating a new TCP connection, possibly start TLS, do the HTTP upgrade handshake to get the WebSocket connection, possibly establish an application protocol on top of that, and then authenticate the connection and connect it to a user profile on the backend. Depending on the device and connection the user is on, this may result in noticeable performance increases.

Since not only the connection would be held, but JavaScript can also be executed outside of the page, this could then also allow a decoupling of long-running network actions from the page lifecycle. An example of this would be to start the upload of a large file on one page, and allow navigation across the entire site while this continues.

The shared nature also points towards another use case: Sharing a single WebSocket connection across multiple tabs of the same Web site or Web app. Benefits here would be reduction of required server resources (less connections), and the possibility to co-ordinate bandwidth use to the server within the application instead of relying on TCP flow control.

(Very) Limited support

A quick look at caniuse.com reveals how limited any potential solution would be at the moment: We're deep into WebKit/Blink-only territory here, and even that's not universal.

overview of browser support for shared web workers on caniuse.com
That's a whole lot of red

And while Firefox has announced support, their regular Web workers presently don't support WebSocket connections - and it's unknown when this will be fixed.

A test case

Still, the idea is interesting, and shared Web workers are worthy of consideration in their own right. Practical use may be limited at the moment, but that doesn't prevent a bit of exploring.

The test case is simple:

  • Two pages, with one linking to the other. Both pages instantiate the same JS script as a shared Web worker.
  • The first page sends a message to the worker which triggers setting a variable persist to true.
  • The first page also establishes a WebSocket connection to an Echo server and periodically sends a heartbeat
  • The second page requests the current value of persist and displays this.
  • Both pages display the WebSocket heartbeat.

The code for this is based on the example for shared Web workers from sitepoint.

Here's the basic code for instantiating the Web worker, adding an event listener for messages to its port, calling 'start' on the port and then sending a message to it:

   var worker = new SharedWorker("worker.js");

   worker.port.addEventListener("message", function(e) {
      // process messages
   }, false);

   worker.port.start();

   worker.port.postMessage("myMessageContent");
   

In the Web worker, after the worker connects, here's the basic code for listening for messages and sending a message back.

   self.addEventListener("connect", function (e) {
      var port = e.ports[0];
      port.start();

      port.addEventListener("message", function (e) {
         port.postMessage("response");
      }, false);

   }, false);
   

The full code for the test case is on github. You'll also find instructions regarding the WebSocket echo server.

I ran the test case in both Chrome (35.0.1867.2) and Opera (19.0.1326.63).

Note: When modifying the test case, I'd definitely recommend Opera for testing. Chrome requires deleting the browser cache in order for changes to the shared Web worker code to be effective.

The good news first: Multi-tab persistence

The test start was opening page_01. A page_02 in a separate tab displayed the set value for persist. Both pages displayed the WebSocket heartbeat counter.

As long as at least one tab (either page_01 or page_02) remained open, any new page_02 tabs opened also displayed persist as true, and the WebSocket heartbeat continued its count.

Persistence while navigating: problematic

Again, the test is started by opening page_01. Then the second page is loaded in the same tab via the link.

With Opera, this always resulted in the shared Web worker being closed and re-instantiated for the second page. persist was always false, and there was no WebSocket heartbeat.

With Chrome, things were more complicated. When working with just the single tab from the beginning, things were as they were in Opera: No persistence.

Initially that was it, but after a browser update, it was possible to get the shared Web Worker to sometimes persist. An (unreliable) trigger I found was to start with two tabs and then close the tab that was opened second(!). The shared Web worker then persisted across reloads and even, sometimes, when closing the tab and opening a new one! As my echo server log output showed, new heartbeats continued for minutes.

All in all that's more than discouraging. The desired behavior appeared in a borderline case far removed from real-world usage, was inconsitent, and changed across versions of the same browser.

The spec: persistence not guaranteed

A look at the spec shows that, as things are, this behaviour won't become more predictable. The main relevant points regarding shared Web worker lifecycle are:

  • Web workers have a document list where the documents that invoked them are listed.
  • Once this list is empty (i.e. the last document/tab from which they were invoked) has been closed, implementations are required to set the worker's closing flag to true.
  • The spec is silent regarding how implementations handle the closing itself.

This means that the setting of the closing flag does not necessarily equate to the worker being closed immediately. If and when this happens is up to the particular browser implementation.

A worker may or may not be closed before it is invoked by a newly loaded page. The closing is more likely to happen than not, but dependent on implementation details and the particulars of the situation at runtime.

The spec does not cover our use case with persistence across page loads in a single tab.

No easy way out

It's clear that there is a need to close orphan workers to recover processing capacity and memory.

The question is then whether there is a simple enough rule that can be added to the spec which would enable the single-tab persistence use case.

The trigger for a special behaviour would be that the next page to be loaded is of the same origin as the present page.

In this case, a (shared) Web worker initiated by the the first page would initially be persisted.

The real question would then be what the conditions for the closing of the worker would be. The newly loaded page might have a delayed instantiation of the worker - or never do so at all.

A simple timeout would, besides the obvious ugliness, not be reliable enough in view of the wide range of possible network and processing delays for the next page, or else wastefully long for the vast majority of use cases.

A canonical event would be needed instead, e.g. if the entire page has loaded, and no inline script has instantiated the same worker script.

I filed an enhancement request regarding this with the W3C WebAppsWG. So in case you have a use case or some other contribution, right there (or on the mailing list) is the place where your comments would have most impact!

Summary

Shared Web workers are usable to share a single WebSocket connection across multiple tabs - in those browsers that support them in principle. Browser support will remain problematic for a while, but the present spec covers this use case.

What shared Web workers do not offer is persistent connections across page loads in a single tab. The present spec excludes this use case.

Start experimenting and prototyping for IoT applications with Crossbar.io and Raspberry Pi!

Loaded with all the software you need to make the board part of WAMP applications!



Learn more

Recent posts

Atom Feed

Search this Site

Stay Informed

Sign up for our newsletter to stay informed of new product releases and features: