Websockets streaming magic — Loading thousands of images in Vue.js.
By: Thomas Underwood
Tags: Full Stack, Front End Development, Vuejs, Web Development, Cloud Computing

Eikona photo hosting service (prototype)

In my last article, I briefly talked about the importance of inexpensive, scalable, and most importantly well-performing cloud-based object storage. Then I took the time to walk you through how to set up and import your data to B2, which I am currently using. In this post, I will walk you through how I am able to optimally load thousands of images without any loading indicators, screens, or a break in the user experience (UX). If you want to follow along and catch up with where we are now, feel free to check out my last article on B2.

The task: load thousands of images from a user's photo gallery. The problem: speed, different hardware and network configurations, and cost. Let me give you a brief overview of how we’re tackling these different issues.

Performance

For performance, we do several things. The first is that we pre-process user images as they are being uploaded. This means that as a user is uploading their images to Eikona, we create a thumbnail that is a scaled-down version of the full resolution image. We store the original image in B2, and we keep the drastically reduced in size thumbnail image (1/10th or less the size of the original photo) on the local disk of our server. When our app loads we serve the thumbnails. This allows us to load thousands of images quickly without degrading the user experience.

There is one last performance improvement that I’ve currently implemented. Instead of bundling the images with Webpack directly, or doing thousands of HTTP requests at once and overloading our browser, we keep a persistent WebSocket connection open between the front-end and back-end services. This allows us to stream large amounts of data in a short amount of time and makes the UX and data fetching appear seamless.

Loading 2584 images at once (~4s)

Cost

By using an inexpensive cloud-based object storage solution like B2 for the full resolution images we are able to store 100’s GBs of photos for only a few dollars a month. Another cost-benefit of using a solution like B2 is that we only have to keep the thumbnails on the server running the back-end instead of the full resolution images. This significantly reduces the cost of operation because server-based block storage is extremely expensive compared to cloud-based object storage for large amounts of data like photos or video.

*We may even be able to get away with downloading and removing the thumbnails before and after each session depending on the speed of our internet connection and the size of the user’s photo gallery.

Varying Hardware and Networks

This is an interesting problem, and I have only just begun tackling it. Most of the performance improvements should help this problem as well. One of the performance improvements that I mentioned, since we are using WebSockets, is now we have the ability to stream data to the front-end. This is extremely powerful for devices that have a slower internet connection or less powerful hardware. Streaming data allows us to batch the data into smaller bite-sized pieces, and only send what is requested or needed at a particular point in time. The largest performance impact of WebSockets is the initial connection, after that everything else is extremely fast. It’s important, however, to keep track of what data has been sent to the UI and what data still needs to be streamed, unlike HTTP requests where you generally fetch everything at once or try to break data up based on a REST convention. You now have to implement something akin to pagination but at the back-end services level. It’s also important to cover different edge cases such as disconnects due to poor internet connections or browser tabs being closed, browser refreshes, signing out, and so forth.

Streaming 175 pictures per request (<1s)

Websocket Implementation Walk-Through

*Please note this is prototype code and doesn’t have authentication or other considerations that are necessary for final production code.

Front-end Perspective

Let's break it down step by step.

created()

In the created life cycle hook before any components are mounted to the document object model (DOM) and displayed for the user, the WebSocket connection starts with the first use of the this.socket object instance where socket.io is waiting for the connect event emit from the back-end. Then there’s the first event emitted from the client (Front-end) Websocket to /requestThumbnails to load the first 175 thumbnail images before the page is loaded. This makes it appear to the user that there was little to no loading time. Finally, you’ll notice the event listener for beforeunload. This event listener is necessary to reset the currently loaded thumbnails in the event of a browser refresh.

mounted()

Once the UI is mounted to the DOM with Vue.js, the mounted life cycle hook is called and the following code is executed. First, the socket is waiting for the response from /respondThumbnails. The first time that /respondThumbnails is triggered will be in response to the first /requestThumbnails event that was emitted in the created life cycle hook. It’s important to note that all this.socket.on events are always active within the mounted lifecycle hook since that is the current stage of the Vue.js lifecycle that is active while the user is viewing the page. Once /respondThumbails receives an event, the new thumbnail data from the back-end is added to the current images, and the entered state is set to false. The entered state is used with the images loaded progress bar which I will explain next. Below is a link to W3schools where I got the code for the progress bar.

Front-end Websocket Code

Scroll Progress Bar — Websocket Streaming Magic

I stumbled across the idea for using this scroll progress bar in order to visually show the user where they are in the viewing process of their photos in relation to how many of their photos have loaded. Many people use an Observer (an invisible DOM element) and wait for the user to scroll within the range of the Observer before requesting and/or loading more data on the UI. I found that this approach leads to a worse user experience (UX) than I had hoped for. I will write an article soon about my findings. In the meantime, I found that using the progress bar scroll calculations helps determine where the user is on the page in relation to how far they have left to scroll. This is more helpful and creates a better UX than the Observer lazy loading pattern. Let’s go back to how the scroll progress bar gave me the idea to create a smoother UX compared to using the Observer lazy loading pattern. As the user scrolls, the window.onscroll event triggers and the corresponding function in the code below runs. Once the user scrolls past 75% of the page height, the /requestThumbnails event emits to the back-end. The idea is that the corresponding response /respondThumbnails returns before the user gets to 100% of the page height, except in the event that all of the images have loaded. The entered variable controls whether or not /requestThumbnails can be called. This ensures that multiple calls to /requestThumbnails do not occur.

Back-end Perspective

I used makeMetaData to create an array of arrays of the size specified — currently 175. This means that the number of images that return to the UI with each /requestThumbnails event will be of the size 175. loadThumbNails takes the chunked thumbnails metadata (175 images) and loads the thumbnails into memory from the server hard drive. /requestThumbnails goes through all of the metaDataArrays (175 chunks of images each), loads the corresponding thumbnails based on the metadata, and responds to the UI with the thumbnails. The code then advances the index. Since the streaming of the image data only needs to go forward and we know the initial index, it does not require us to use a loop. Finally, we reset the state of everything back to the initial state in the disconnect socket event.

Back-end Websocket Code

I hope you enjoyed this article about Websockets and obtaining good performance, cost, and UX for web apps. In my next article, I’ll address the pros and cons of the Observer-based lazy loading pattern vs the scroll height calculation method that I used.