{"id":1206,"date":"2025-04-23T10:00:00","date_gmt":"2025-04-23T10:00:00","guid":{"rendered":"http:\/\/www.azxyfun.com\/?p=1206"},"modified":"2025-05-01T09:25:38","modified_gmt":"2025-05-01T09:25:38","slug":"building-an-offline-friendly-image-upload-system","status":"publish","type":"post","link":"http:\/\/www.azxyfun.com\/index.php\/2025\/04\/23\/building-an-offline-friendly-image-upload-system\/","title":{"rendered":"Building An Offline-Friendly Image Upload System"},"content":{"rendered":"

Building An Offline-Friendly Image Upload System<\/title><\/p>\n<article>\n<header>\n<h1>Building An Offline-Friendly Image Upload System<\/h1>\n<address>Amejimaobari Ollornwi<\/address>\n<p> 2025-04-23T10:00:00+00:00<br \/>\n 2025-05-01T09:03:46+00:00<br \/>\n <\/header>\n<p>So, you\u2019re filling out an online form, and it asks you to upload a file. You click the input, select a file from your desktop, and are good to go. But something happens. The network drops, the file disappears, and you\u2019re stuck having to re-upload the file. <strong>Poor network connectivity<\/strong> can lead you to spend an unreasonable amount of time trying to upload files successfully.<\/p>\n<p>What ruins the user experience stems from having to constantly check network stability and retry the upload several times. While we may not be able to do much about network connectivity, as developers, we can always do something to ease the pain that comes with this problem.<\/p>\n<p>One of the ways we can solve this problem is by tweaking image upload systems in a way that enables users to upload images offline — <strong>eliminating the need for a reliable network connection<\/strong>, and then having the system retry the upload process when the network becomes stable, without the user intervening.<\/p>\n<p>This article is going to focus on explaining how to build <strong>an offline-friendly image upload system<\/strong> using PWA (progressive web application) technologies such as <code>IndexedDB<\/code>, service workers, and the Background Sync API. We will also briefly cover tips for improving the user experience for this system.<\/p>\n<h2 id=\"planning-the-offline-image-upload-system\">Planning The Offline Image Upload System<\/h2>\n<p>Here\u2019s a flow chart for an offline-friendly image upload system.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/building-offline-friendly-image-upload-system\/1-upload-system-flow-chart.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"678\" src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/building-offline-friendly-image-upload-system\/1-upload-system-flow-chart.png\" alt=\"Flow chart of an offline-friendly image upload system\" \/><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n Flow chart of an offline-friendly image upload system (<a href=\"https:\/\/files.smashing.media\/articles\/building-offline-friendly-image-upload-system\/1-upload-system-flow-chart.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>As shown in the flow chart, the process unfolds as follows:<\/p>\n<ol>\n<li><strong>The user selects an image.<\/strong><br \/>\nThe process begins by letting the user select their image.<\/li>\n<li><strong>The image is stored locally in <code>IndexedDB<\/code>.<\/strong><br \/>\nNext, the system checks for network connectivity. If network connectivity is available, the system uploads the image directly, avoiding unnecessary local storage usage. However, if the network is not available, the image will be stored in <code>IndexedDB<\/code>.<\/li>\n<li><strong>The service worker detects when the network is restored.<\/strong><br \/>\nWith the image stored in <code>IndexedDB<\/code>, the system waits to detect when the network connection is restored to continue with the next step.<\/li>\n<li><strong>The background sync processes pending uploads.<\/strong><br \/>\nThe moment the connection is restored, the system will try to upload the image again.<\/li>\n<li><strong>The file is successfully uploaded<\/strong>.<br \/>\nThe moment the image is uploaded, the system will remove the local copy stored in <code>IndexedDB<\/code>.<\/p>\n<p><\/li>\n<\/ol>\n<div data-audience=\"non-subscriber\" data-remove=\"true\" class=\"feature-panel-container\">\n<aside class=\"feature-panel\">\n<div class=\"feature-panel-left-col\">\n<div class=\"feature-panel-description\">\n<p>Meet <strong><a data-instant href=\"https:\/\/www.smashingconf.com\/online-workshops\/\">Smashing Workshops<\/a><\/strong> on <strong>front-end, design & UX<\/strong>, with practical takeaways, live sessions, <strong>video recordings<\/strong> and a friendly Q&A. With Brad Frost, St\u00e9ph Walter and <a href=\"https:\/\/smashingconf.com\/online-workshops\/workshops\">so many others<\/a>.<\/p>\n<p><a data-instant href=\"smashing-workshops\" class=\"btn btn--green btn--large\">Jump to the workshops \u21ac<\/a><\/div>\n<\/div>\n<div class=\"feature-panel-right-col\"><a data-instant href=\"smashing-workshops\" class=\"feature-panel-image-link\"><\/p>\n<div class=\"feature-panel-image\">\n<img loading=\"lazy\" class=\"feature-panel-image-img\" src=\"\/images\/smashing-cat\/cat-scubadiving-panel.svg\" alt=\"Feature Panel\" width=\"257\" height=\"355\" \/><\/p>\n<\/div>\n<p><\/a>\n<\/div>\n<\/aside>\n<\/div>\n<h2 id=\"implementing-the-system\">Implementing The System<\/h2>\n<p>The first step in the system implementation is allowing the user to select their images. There are different ways you can achieve this:<\/p>\n<ul>\n<li>You can use a simple <code><input type="file"><\/code> element;<\/li>\n<li>A drag-and-drop interface.<\/li>\n<\/ul>\n<p>I would advise that you use both. Some users prefer to use the drag-and-drop interface, while others think the only way to upload images is through the <code><input type="file"><\/code> element. Having both options will help improve the user experience. You can also consider allowing users to paste images directly in the browser using the Clipboard API.<\/p>\n<h3 id=\"registering-the-service-worker\">Registering The Service Worker<\/h3>\n<p>At the heart of this solution is the <a href=\"https:\/\/www.smashingmagazine.com\/2016\/02\/making-a-service-worker\/\">service worker<\/a>. Our service worker is going to be responsible for retrieving the image from the <code>IndexedDB<\/code> store, uploading it when the internet connection is restored, and clearing the <code>IndexedDB<\/code> store when the image has been uploaded.<\/p>\n<p>To use a service worker, you first have to register one:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('\/service-worker.js')\n .then(reg => console.log('Service Worker registered', reg))\n .catch(err => console.error('Service Worker registration failed', err));\n}\n<\/code><\/pre>\n<\/div>\n<h3 id=\"checking-for-network-connectivity\">Checking For Network Connectivity<\/h3>\n<p>Remember, the problem we are trying to solve is caused by <strong>unreliable network connectivity<\/strong>. If this problem does not exist, there is no point in trying to solve anything. Therefore, once the image is selected, we need to check if the user has a reliable internet connection before registering a sync event and storing the image in <code>IndexedDB<\/code>.<\/p>\n<pre><code class=\"language-javascript\">function uploadImage() {\n if (navigator.onLine) {\n \/\/ Upload Image\n } else {\n \/\/ register Sync Event\n \/\/ Store Images in IndexedDB\n }\n}\n<\/code><\/pre>\n<p><strong>Note<\/strong>: I\u2019m only using the <code>navigator.onLine<\/code> property here to demonstrate how the system would work. The <code>navigator.onLine<\/code> property is <strong>unreliable<\/strong>, and I would suggest you come up with a custom solution to check whether the user is connected to the internet or not. One way you can do this is by sending a ping request to a server endpoint you\u2019ve created.<\/p>\n<h3 id=\"registering-the-sync-event\">Registering The Sync Event<\/h3>\n<p>Once the network test fails, the next step is to register a sync event. The sync event needs to be registered at the point where the system fails to upload the image due to a poor internet connection.<\/p>\n<pre><code class=\"language-javascript\">async function registerSyncEvent() {\n if ('SyncManager' in window) {\n const registration = await navigator.serviceWorker.ready;\n await registration.sync.register('uploadImages');\n console.log('Background Sync registered');\n }\n}\n<\/code><\/pre>\n<p>After registering the sync event, you need to listen for it in the service worker.<\/p>\n<pre><code class=\"language-javascript\">self.addEventListener('sync', (event) => {\n if (event.tag === 'uploadImages') {\n event.waitUntil(sendImages());\n }\n});\n<\/code><\/pre>\n<p>The <code>sendImages<\/code> function is going to be an asynchronous process that will retrieve the image from <code>IndexedDB<\/code> and upload it to the server. This is what it\u2019s going to look like:<\/p>\n<pre><code class=\"language-javascript\">async function sendImages() {\n try {\n \/\/ await image retrieval and upload\n } catch (error) {\n \/\/ throw error\n }\n}\n<\/code><\/pre>\n<h3 id=\"opening-the-database\">Opening The Database<\/h3>\n<p>The first thing we need to do in order to store our image locally is to open an <code>IndexedDB<\/code> store. As you can see from the code below, we are creating <strong>a global variable to store the database instance<\/strong>. The reason for doing this is that, subsequently, when we want to retrieve our image from <code>IndexedDB<\/code>, we wouldn\u2019t need to write the code to open the database again.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">let database; \/\/ Global variable to store the database instance\n\nfunction openDatabase() {\n return new Promise((resolve, reject) => {\n if (database) return resolve(database); \/\/ Return existing database instance \n\n const request = indexedDB.open(\"myDatabase\", 1);\n\n request.onerror = (event) => {\n console.error(\"Database error:\", event.target.error);\n reject(event.target.error); \/\/ Reject the promise on error\n };\n\n request.onupgradeneeded = (event) => {\n const db = event.target.result;\n \/\/ Create the \"images\" object store if it doesn't exist.\n if (!db.objectStoreNames.contains(\"images\")) {\n db.createObjectStore(\"images\", { keyPath: \"id\" });\n }\n console.log(\"Database setup complete.\");\n };\n\n request.onsuccess = (event) => {\n database = event.target.result; \/\/ Store the database instance globally\n resolve(database); \/\/ Resolve the promise with the database instance\n };\n });\n}\n<\/code><\/pre>\n<\/div>\n<div class=\"partners__lead-place\"><\/div>\n<h3 id=\"storing-the-image-in-indexeddb\">Storing The Image In IndexedDB<\/h3>\n<p>With the <code>IndexedDB<\/code> store open, we can now store our images.<\/p>\n<blockquote><p>Now, you may be wondering why an easier solution like <code>localStorage<\/code> wasn\u2019t used for this purpose.<\/p>\n<p>The reason for that is that <code>IndexedDB<\/code> operates asynchronously and doesn\u2019t block the main JavaScript thread, whereas <code>localStorage<\/code> runs synchronously and can block the JavaScript main thread if it is being used.<\/p><\/blockquote>\n<p>Here\u2019s how you can store the image in <code>IndexedDB<\/code>:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">async function storeImages(file) {\n \/\/ Open the IndexedDB database.\n const db = await openDatabase();\n \/\/ Create a transaction with read and write access.\n const transaction = db.transaction(\"images\", \"readwrite\");\n \/\/ Access the \"images\" object store.\n const store = transaction.objectStore(\"images\");\n \/\/ Define the image record to be stored.\n const imageRecord = {\n id: IMAGE_ID, \/\/ a unique ID\n image: file \/\/ Store the image file (Blob)\n };\n \/\/ Add the image record to the store.\n const addRequest = store.add(imageRecord);\n \/\/ Handle successful addition.\n addRequest.onsuccess = () => console.log(\"Image added successfully!\");\n \/\/ Handle errors during insertion.\n addRequest.onerror = (e) => console.error(\"Error storing image:\", e.target.error);\n}\n<\/code><\/pre>\n<\/div>\n<p>With the images stored and the background sync set, the system is ready to upload the image whenever the network connection is restored.<\/p>\n<h3 id=\"retrieving-and-uploading-the-images\">Retrieving And Uploading The Images<\/h3>\n<p>Once the network connection is restored, the sync event will fire, and the service worker will retrieve the image from <code>IndexedDB<\/code> and upload it.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">async function retrieveAndUploadImage(IMAGE_ID) {\n try {\n const db = await openDatabase(); \/\/ Ensure the database is open\n const transaction = db.transaction(\"images\", \"readonly\");\n const store = transaction.objectStore(\"images\");\n const request = store.get(IMAGE_ID);\n request.onsuccess = function (event) {\n const image = event.target.result;\n if (image) {\n \/\/ upload Image to server here\n } else {\n console.log(\"No image found with ID:\", IMAGE_ID);\n }\n };\n request.onerror = () => {\n console.error(\"Error retrieving image.\");\n };\n } catch (error) {\n console.error(\"Failed to open database:\", error);\n }\n}\n<\/code><\/pre>\n<\/div>\n<h3 id=\"deleting-the-indexeddb-database\">Deleting The IndexedDB Database<\/h3>\n<p>Once the image has been uploaded, the <code>IndexedDB<\/code> store is no longer needed. Therefore, it should be deleted along with its content to free up storage.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">function deleteDatabase() {\n \/\/ Check if there's an open connection to the database.\n if (database) {\n database.close(); \/\/ Close the database connection\n console.log(\"Database connection closed.\");\n }\n\n \/\/ Request to delete the database named \"myDatabase\".\n const deleteRequest = indexedDB.deleteDatabase(\"myDatabase\");\n\n \/\/ Handle successful deletion of the database.\n deleteRequest.onsuccess = function () {\n console.log(\"Database deleted successfully!\");\n };\n\n \/\/ Handle errors that occur during the deletion process.\n deleteRequest.onerror = function (event) {\n console.error(\"Error deleting database:\", event.target.error);\n };\n\n \/\/ Handle cases where the deletion is blocked (e.g., if there are still open connections).\n deleteRequest.onblocked = function () {\n console.warn(\"Database deletion blocked. Close open connections and try again.\");\n };\n}\n<\/code><\/pre>\n<\/div>\n<p>With that, the entire process is complete!<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"considerations-and-limitations\">Considerations And Limitations<\/h2>\n<p>While we\u2019ve done a lot to help improve the experience by supporting offline uploads, the system is not without its limitations. I figured I would specifically call those out because it\u2019s worth knowing where this solution might fall short of your needs.<\/p>\n<ul>\n<li><strong>No Reliable Internet Connectivity Detection<\/strong><br \/>\nJavaScript does not provide a foolproof way to detect online status. For this reason, you need to come up with a custom solution for detecting online status.<\/li>\n<li><strong>Chromium-Only Solution<\/strong><br \/>\nThe Background Sync API is currently <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Background_Synchronization_API#browser_compatibility\">limited to Chromium-based browsers<\/a>. As such, this solution is only supported by Chromium browsers. That means you will need a more robust solution if you have the majority of your users on non-Chromium browsers.<\/li>\n<li><strong><code>IndexedDB<\/code> Storage Policies<\/strong><br \/>\nBrowsers impose storage limitations and eviction policies for <code>IndexedDB<\/code>. For instance, in Safari, data stored in <code>IndexedDB<\/code> has a lifespan of seven days if the user doesn\u2019t interact with the website. This is something you should bear in mind if you do come up with an alternative for the background sync API that supports Safari.<\/li>\n<\/ul>\n<h2 id=\"enhancing-the-user-experience\">Enhancing The User Experience<\/h2>\n<p>Since the entire process happens in the background, we need a way to inform the users when images are stored, waiting to be uploaded, or have been successfully uploaded. Implementing certain <strong>UI elements<\/strong> for this purpose will indeed enhance the experience for the users. These UI elements may include toast notifications, upload status indicators like spinners (to show active processes), progress bars (to show state progress), network status indicators, or buttons to provide retry and cancel options.<\/p>\n<h2 id=\"wrapping-up\">Wrapping Up<\/h2>\n<p>Poor internet connectivity can disrupt the user experience of a web application. However, by leveraging PWA technologies such as <code>IndexedDB<\/code>, service workers, and the Background Sync API, developers can help improve the reliability of web applications for their users, especially those in areas with unreliable internet connectivity.<\/p>\n<div class=\"signature\">\n <img src=\"https:\/\/www.smashingmagazine.com\/images\/logo\/logo--red.png\" alt=\"Smashing Editorial\" width=\"35\" height=\"46\" loading=\"lazy\" \/><br \/>\n <span>(gg, yk)<\/span>\n<\/div>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>Building An Offline-Friendly Image Upload System Building An Offline-Friendly Image Upload System Amejimaobari Ollornwi 2025-04-23T10:00:00+00:00 2025-05-01T09:03:46+00:00 So, you\u2019re filling out an online form, and it asks you to upload a file. You click the input, select a file from your desktop, and are good to go. But something happens. The network drops, the file disappears, […]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"_links":{"self":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts\/1206"}],"collection":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/comments?post=1206"}],"version-history":[{"count":1,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts\/1206\/revisions"}],"predecessor-version":[{"id":1207,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts\/1206\/revisions\/1207"}],"wp:attachment":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/media?parent=1206"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/categories?post=1206"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/tags?post=1206"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}