What we’re building We’ll build a cross-platform mobile app for taking photos and uploading to firebase. In , we saw how to take a picture and save it to Firebase Cloud Storage. In this post we’ll move the uploading to a separate thread via web worker, and use the blueimp library to generate a thumbnail locally and show it while uploading. Part 1 Web Workers What is a web worker Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. ( ) mozilla.org So we’ll use a web worker to offload code that can potentially block our UI thread — in our case the Firebase Storage code — to another thread. Configuring workerize-loader We’ll use to make using web workers a little easier (web workers interface is a little weird). We need to add some webpack configuration to tell webpack to use . In in the section add: workerize-loader yarn add workerize-loader workerize-loader quasar.conf.js build extendWebpack(cfg) { cfg.module.rules.push({ : , : , }) }, chainWebpack (chain, { isServer, isClient }) { chain.output.globalObject( ) } test /\.worker\.js$/ loader 'workerize-loader' 'self' This tells webpack to load the file using . worker.js workerize-loader Moving code to the worker Let’s add the file and move our uploading code into it: src/services/worker.js cloudStorage { storageId = ().getTime().toString(); downloadURL = cloudStorage.uploadBase64( imageData, storageId ); { storageId, downloadURL, } } { cloudStorage.initialize(); } { cloudStorage.deleteFromStorage(storageId) } import from './cloud-storage' export async ( ) function uploadPicture imageData let new Date let await return export ( ) function initFB export async ( ) function deletePic storageId await We’ve also added a call which we’ll see later in . deletePic cloudStorage.js Adding simple state management We’ll want to create an instance of the worker and save it in the app’s state. We don’t have state management yet, so let’s add a simple state management pattern using a file: store.js worker ; store = { : , : { : [], : }, initWorker() { .state.workerInstance = worker(); .state.workerInstance.initFB(); }, loadPictures() { ( .debug) .log( ) picsJson = localStorage.getItem( ); (!picsJson) .state.pics = []; .state.pics = .parse(picsJson); .state.pics = .state.pics.filter( !pic.uploading) }, addPic(pic) { ( .debug) .log( , pic) pic.failed = ; .state.pics.splice( , , pic); localStorage.setItem( , .stringify( .state.pics)); }, deletePic(idx) { ( .debug) .log( , idx) .state.pics[idx].uploading = ; ( .state.pics[idx].storageId) { .state.workerInstance.deletePic( .state.pics[idx].storageId) } .state.pics.splice(idx, ); localStorage.setItem( , .stringify( .state.pics)); }, updatePicUploaded(oldPic, newPic) { ( .debug) .log( , oldPic, newPic) oldPic.uploading = oldPic.url = newPic.downloadURL oldPic.storageId = newPic.storageId oldPic.width = newPic.width oldPic.height = newPic.height localStorage.setItem( , .stringify( .state.pics)); }, updatePicFailed(pic) { ( .debug) .log( , pic) pic.failed = }, } { ...store } import from "workerize-loader!./worker.js" var debug true state pics uploading false this this async if this console "loadPictures triggered" let await "pics" if this else this JSON this this => pic async if this console "addPic triggered with" false this 0 0 "pics" JSON this async if this console "deletePic triggered with" this true if this await this this this 1 "pics" JSON this async if this console "updatePicUploaded triggered with" false "pics" JSON this async if this console "updatePicFailed triggered with" true export default A few things going on here: We’re importing our worker using the prefix workerize-loader! which tells webpack to use the loader we configured earlier. We moved the pics collection to the state object from Index.vue. We exposed the method initWorker which initializes the worker instance. We added some CRUD methods for persisting the pics collection in localStorage: addPic, loadPictures and delete. updatePicUploaded and updatePicFailed changes the loading property of the picture. We’ll use this to show the spinner. Generating thumbnails We’re going to generate a thumbnail (on the client) to show while the image is uploading. We’ll use the library for this: blueimp yarn add blueimp-load-image Let’s add another service for manipulating images: src/services/image-ops.js: loadImage base64JpegPrefix = ; { base64Str.substr(base64Str.indexOf( ) + ); } { base64JpegPrefix + imageData } { ( resolve => { url = base64JpegPrefix + imageData res = fetch(url) blob = res.blob() loadImage( blob, (canvas) => { dataURL = canvas.toDataURL( ); resolve(removeBase64Prefix(dataURL)); }, { : maxWidth, : } ); }); } { removeBase64Prefix, generateThumbnail, addBase64Prefix, }; import from 'blueimp-load-image' const "data:image/jpeg;base64," ( ) function removeBase64Prefix base64Str return "," 1 ( ) function addBase64Prefix imageData return async ( ) function generateThumbnail imageData, maxWidth return new Promise async let let await let await let 'image/jpeg' maxWidth canvas true export default Adding the image-uploader service We’ll extract all the image uploading flow to a service to keep our UI component clean. Add the file src/services/image-uploader.js: store ; cordovaCamera ; imageOps ; config ; { base64 = cordovaCamera.getBase64FromCamera(); imageData = imageOps.removeBase64Prefix(base64); thumbnailImageData = imageOps.generateThumbnail( imageData, config.photos.thumbnailMaxWidth ); localPic = { : imageOps.addBase64Prefix(thumbnailImageData), : }; store.addPic(localPic); uploadedPic = store.state.workerInstance.uploadPicture( imageData ); store.updatePicUploaded(localPic, uploadedPic); } { uploadImageFromCamera } import from "./store" import from "./cordova-camera" import from "./image-ops" import from "./config" async ( ) function uploadImageFromCamera let await let let await let url uploading true let await export default What we’re doing in the uploadImageFromCamera method is: getting base64 from the cordova camera -> generating thumbnails using our imageOps -> generating a new pic object in our state with uploading=true -> uploading the picture to Firebase using the web worker -> updating the pic’s uploading state property when uploading is finished. Putting it together Now we’ve added all these services, we need to call them from our components. In App.vue, we’ll use the mounted lifecycle hook to initialize the worker and load the saved picture urls from the saved state: <template> <router-view /> </div> <script> store ; { : , mounted() { store.initWorker(); store.loadPictures(); } }; <style> < = > div id "q-app" </ > template import from "./services/store.js" export default name "App" async </ > script </ > style Finally, we’ll add the interface for viewing all this in Index.vue: < > template < > q-page class < = > div class "q-pa-md" < = = = > div class "row justify-center q-ma-md" v-for "(pic, idx) in pics" :key "idx" < = > div class "col" < = > q-card v-if "pic" < = = > q-img spinner-color "white" :src "pic.url" < = = > div class "spinner-container" v-if "pic.uploading && !pic.failed" < = = /> q-spinner color "white" size "4em" </ > div < = = > div class "spinner-container" v-if "pic.failed" < = = > q-icon name "cloud_off" style "font-size: 48px;" </ > q-icon </ > div </ > q-img < = > q-card-actions align "around" < = = @ = /> q-btn flat round color "red" icon "favorite" click "notifyNotImplemented()" < = = @ = /> q-btn flat round color "teal" icon "bookmark" click "notifyNotImplemented()" < = = @ = = /> q-btn flat round color "primary" icon "delete" click "deletePic(idx)" :disable "pic.uploading" </ > q-card-actions </ > q-card </ > div </ > div </ > div </ > q-page </ > template < > style scoped { : flex; : center; : center; : ; : ; } .spinner-container display align-items justify-content width 100% height 100% </ > style < > script store ; { EventBus } ; imageUploader ; { : , data() { { : store.state }; }, mounted() { EventBus.$off( ); EventBus.$on( , .uploadImageFromCamera); }, : { pics() { .state.pics; } }, : { notifyNotImplemented() { .$q.notify({ : }); }, deletePic(idx) { { store.deletePic(idx); .$q.notify({ : }); } (err) { .error(err); .$q.notify({ : }); } }, uploadImageFromCamera() { { imageUploader.uploadImageFromCamera(); } (err) { .error( ); .dir(err); store.updatePicFailed(localPic); .$q.notify({ : }); } } } }; import from "../services/store" import from "../services/event-bus" import from "../services/image-uploader" export default name "PageIndex" return state "takePicture" "takePicture" this computed return this methods this message "Not implemented yet :/" async try await this message "Picture deleted." catch console this message "Delete failed. Check log." async try catch console "Uploading failed" console this message "Uploading failed. Check log." </ > script What’s going on here: We’ve added a q-spinner to show when the uploading property of the picture is true. We’re calling imageUploader.uploadImageFromCamera when clicking on the photo button, which handles our uploading. We’ve added some actions to the q-card of the picture (only delete is implemented for now) Final app That’s it! We have an image uploading app that shows a blurred thumbnails with a spinner, a little like WhatsApp’s image upload. Running all this using quasar dev -m android/ios will show the final result: Further improvements Some ideas for improving this app further would be: Offload thumbnail creation to another web worker Handle long lists of images with a virtual list component like this one Add a carousel / gallery component for viewing images full-size Add a retry mechanism for failed uploads Use — so that users can only delete their own photos firebase auth Source code The full code is on GitHub here: . vue-firebase-image-upload Enjoy :)