From 36e5d298dbb045de4e22a3b64b3fb59209bc4ebf Mon Sep 17 00:00:00 2001
From: martin <74269598+martabal@users.noreply.github.com>
Date: Sun, 18 Feb 2024 20:18:40 +0100
Subject: [PATCH] perf(web): optimize images and modules (#7088)

* perf: optimize images and modules

* fix: tests

* fix: missing font

* fix: delay showing the loading spinner

* simplify

* simplify

* pr feedback

* chore: merge main

* fix: enum

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
---
 web/package-lock.json                         | 675 ++++++++++++++++++
 web/package.json                              |   1 +
 .../components/album-page/album-card.svelte   |  34 +-
 .../asset-viewer/detail-panel.svelte          |  56 +-
 .../memory-page/memory-viewer.svelte          |  46 +-
 .../shared-components/change-location.svelte  |  31 +-
 .../sharedlinks-page/shared-link-card.svelte  |   2 +-
 web/src/lib/constants.ts                      |   4 +
 web/src/lib/utils/asset-utils.ts              |   4 +
 web/vite.config.js                            |   2 +
 10 files changed, 796 insertions(+), 59 deletions(-)

diff --git a/web/package-lock.json b/web/package-lock.json
index 2c5c7f1d24..c784395565 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -31,6 +31,7 @@
         "@floating-ui/dom": "^1.5.1",
         "@socket.io/component-emitter": "^3.1.0",
         "@sveltejs/adapter-static": "^3.0.1",
+        "@sveltejs/enhanced-img": "^0.1.8",
         "@sveltejs/kit": "^2.5.0",
         "@sveltejs/vite-plugin-svelte": "^3.0.2",
         "@testing-library/jest-dom": "^6.1.5",
@@ -456,6 +457,16 @@
       "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
       "dev": true
     },
+    "node_modules/@emnapi/runtime": {
+      "version": "0.45.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
+      "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.19.11",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
@@ -969,6 +980,456 @@
       "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
       "dev": true
     },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz",
+      "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "glibc": ">=2.26",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz",
+      "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "glibc": ">=2.26",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz",
+      "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "macos": ">=11",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz",
+      "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "macos": ">=10.13",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz",
+      "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.28",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz",
+      "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.26",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz",
+      "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.28",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz",
+      "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.26",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz",
+      "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "musl": ">=1.2.2",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz",
+      "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "musl": ">=1.2.2",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz",
+      "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.28",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz",
+      "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.26",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz",
+      "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.28",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz",
+      "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "glibc": ">=2.26",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz",
+      "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "musl": ">=1.2.2",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz",
+      "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "musl": ">=1.2.2",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.0.1"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz",
+      "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^0.45.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz",
+      "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz",
+      "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+        "npm": ">=9.6.5",
+        "pnpm": ">=7.1.0",
+        "yarn": ">=3.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
     "node_modules/@immich/sdk": {
       "resolved": "../open-api/typescript-sdk",
       "link": true
@@ -1169,6 +1630,34 @@
       "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==",
       "dev": true
     },
+    "node_modules/@rollup/pluginutils": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
+      "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "^1.0.0",
+        "estree-walker": "^2.0.2",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "rollup": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "dev": true
+    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
       "version": "4.9.5",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz",
@@ -1358,6 +1847,17 @@
         "@sveltejs/kit": "^2.0.0"
       }
     },
+    "node_modules/@sveltejs/enhanced-img": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.1.8.tgz",
+      "integrity": "sha512-0cLVR9KiO0/t3VVm64OM7bPHTkdaT2aaz1rwoAhao+EBXR3vMvLoYXLHvz8o9/552PSV8G844RkH7qkGc3YAiQ==",
+      "dev": true,
+      "dependencies": {
+        "magic-string": "^0.30.5",
+        "svelte-parse-markup": "^0.1.2",
+        "vite-imagetools": "^6.2.8"
+      }
+    },
     "node_modules/@sveltejs/kit": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz",
@@ -2966,6 +3466,19 @@
         "periscopic": "^3.1.0"
       }
     },
+    "node_modules/color": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1",
+        "color-string": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=12.5.0"
+      }
+    },
     "node_modules/color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2981,6 +3494,34 @@
       "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
       "dev": true
     },
+    "node_modules/color-string": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "node_modules/color/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
     "node_modules/combined-stream": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3280,6 +3821,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/devalue": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz",
@@ -4617,6 +5167,18 @@
         "node": ">= 4"
       }
     },
+    "node_modules/imagetools-core": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/imagetools-core/-/imagetools-core-6.0.4.tgz",
+      "integrity": "sha512-N1qs5qn7u9nR3kboISkYuvJm8MohiphCfBa+wx1UOropVaFis9/mh6wuDPLHJNhl6/64C7q2Pch5NASVKAaSrg==",
+      "dev": true,
+      "dependencies": {
+        "sharp": "^0.33.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -7013,6 +7575,79 @@
         "node": ">=8"
       }
     },
+    "node_modules/sharp": {
+      "version": "0.33.2",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
+      "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "dependencies": {
+        "color": "^4.2.3",
+        "detect-libc": "^2.0.2",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "libvips": ">=8.15.1",
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.33.2",
+        "@img/sharp-darwin-x64": "0.33.2",
+        "@img/sharp-libvips-darwin-arm64": "1.0.1",
+        "@img/sharp-libvips-darwin-x64": "1.0.1",
+        "@img/sharp-libvips-linux-arm": "1.0.1",
+        "@img/sharp-libvips-linux-arm64": "1.0.1",
+        "@img/sharp-libvips-linux-s390x": "1.0.1",
+        "@img/sharp-libvips-linux-x64": "1.0.1",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.0.1",
+        "@img/sharp-libvips-linuxmusl-x64": "1.0.1",
+        "@img/sharp-linux-arm": "0.33.2",
+        "@img/sharp-linux-arm64": "0.33.2",
+        "@img/sharp-linux-s390x": "0.33.2",
+        "@img/sharp-linux-x64": "0.33.2",
+        "@img/sharp-linuxmusl-arm64": "0.33.2",
+        "@img/sharp-linuxmusl-x64": "0.33.2",
+        "@img/sharp-wasm32": "0.33.2",
+        "@img/sharp-win32-ia32": "0.33.2",
+        "@img/sharp-win32-x64": "0.33.2"
+      }
+    },
+    "node_modules/sharp/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp/node_modules/semver": {
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7066,6 +7701,21 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/simple-swizzle": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+      "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+      "dev": true,
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/simple-swizzle/node_modules/is-arrayish": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "dev": true
+    },
     "node_modules/sirv": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@@ -7551,6 +8201,18 @@
         }
       }
     },
+    "node_modules/svelte-parse-markup": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/svelte-parse-markup/-/svelte-parse-markup-0.1.2.tgz",
+      "integrity": "sha512-DycY7DJr7VqofiJ63ut1/NEG92HrWWL56VWITn/cJCu+LlZhMoBkBXT4opUitPEEwbq1nMQbv4vTKUfbOqIW1g==",
+      "dev": true,
+      "funding": {
+        "url": "https://bjornlu.com/sponsor"
+      },
+      "peerDependencies": {
+        "svelte": "^3.0.0 || ^4.0.0"
+      }
+    },
     "node_modules/svelte-preprocess": {
       "version": "5.1.3",
       "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
@@ -8125,6 +8787,19 @@
         }
       }
     },
+    "node_modules/vite-imagetools": {
+      "version": "6.2.9",
+      "resolved": "https://registry.npmjs.org/vite-imagetools/-/vite-imagetools-6.2.9.tgz",
+      "integrity": "sha512-C4ZYhgj2vAj43/TpZ06XlDNP0p/7LIeYbgUYr+xG44nM++4HGX6YZBKAYpiBNgiCFUTJ6eXkRppWBrfPMevgmg==",
+      "dev": true,
+      "dependencies": {
+        "@rollup/pluginutils": "^5.0.5",
+        "imagetools-core": "^6.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/vite-node": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
diff --git a/web/package.json b/web/package.json
index 50cd310e27..477fa4e9c3 100644
--- a/web/package.json
+++ b/web/package.json
@@ -26,6 +26,7 @@
     "@floating-ui/dom": "^1.5.1",
     "@socket.io/component-emitter": "^3.1.0",
     "@sveltejs/adapter-static": "^3.0.1",
+    "@sveltejs/enhanced-img": "^0.1.8",
     "@sveltejs/kit": "^2.5.0",
     "@sveltejs/vite-plugin-svelte": "^3.0.2",
     "@testing-library/jest-dom": "^6.1.5",
diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte
index 400c7fcfcd..eb645a702d 100644
--- a/web/src/lib/components/album-page/album-card.svelte
+++ b/web/src/lib/components/album-page/album-card.svelte
@@ -1,6 +1,5 @@
 <script lang="ts">
   import { api } from '$lib/api';
-  import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
   import Icon from '$lib/components/elements/icon.svelte';
   import { locale } from '$lib/stores/preferences.store';
   import { user } from '$lib/stores/user.store';
@@ -21,7 +20,7 @@
 
   $: imageData = album.albumThumbnailAssetId
     ? getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp)
-    : noThumbnailUrl;
+    : null;
 
   const dispatchClick = createEventDispatcher<OnClick>();
   const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>();
@@ -50,7 +49,7 @@
     dispatchShowContextMenu('showalbumcontextmenu', getContextMenuPosition(e));
 
   onMount(async () => {
-    imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || noThumbnailUrl;
+    imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || null;
   });
 
   const getAlbumOwnerInfo = () => getUserById({ id: album.ownerId });
@@ -81,15 +80,26 @@
   {/if}
 
   <div class={`relative aspect-square`}>
-    <img
-      loading={preload ? 'eager' : 'lazy'}
-      src={imageData}
-      alt={album.id}
-      class={`z-0 h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg`}
-      data-testid="album-image"
-      draggable="false"
-    />
-    <div class="absolute top-0 h-full w-full rounded-3xl" />
+    {#if album.albumThumbnailAssetId}
+      <img
+        loading={preload ? 'eager' : 'lazy'}
+        src={imageData}
+        alt={album.id}
+        class="z-0 h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg"
+        data-testid="album-image"
+        draggable="false"
+      />
+    {:else}
+      <enhanced:img
+        loading={preload ? 'eager' : 'lazy'}
+        src="$lib/assets/no-thumbnail.png"
+        sizes="min(271px,186px)"
+        alt={album.id}
+        class="z-0 h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg"
+        data-testid="album-image"
+        draggable="false"
+      />
+    {/if}
   </div>
 
   <div class="mt-4">
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 97d3e2dfea..15d46da80b 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -1,14 +1,14 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
   import ChangeDate from '$lib/components/shared-components/change-date.svelte';
-  import { AppRoute, QueryParameter } from '$lib/constants';
+  import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
   import { boundingBoxesArray } from '$lib/stores/people.store';
   import { locale } from '$lib/stores/preferences.store';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { user } from '$lib/stores/user.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
-  import { getAssetFilename } from '$lib/utils/asset-utils';
+  import { delay, getAssetFilename } from '$lib/utils/asset-utils';
   import { autoGrowHeight } from '$lib/utils/autogrow';
   import { clickOutside } from '$lib/utils/click-outside';
   import {
@@ -38,8 +38,8 @@
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import PersonSidePanel from '../faces-page/person-side-panel.svelte';
   import ChangeLocation from '../shared-components/change-location.svelte';
-  import Map from '../shared-components/map/map.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
+  import LoadingSpinner from '../shared-components/loading-spinner.svelte';
 
   export let asset: AssetResponseDto;
   export let albums: AlbumResponseDto[] = [];
@@ -609,27 +609,37 @@
 
 {#if latlng && $featureFlags.loaded && $featureFlags.map}
   <div class="h-[360px]">
-    <Map
-      mapMarkers={[{ lat: latlng.lat, lon: latlng.lng, id: asset.id }]}
-      center={latlng}
-      zoom={15}
-      simplified
-      useLocationPin
-    >
-      <svelte:fragment slot="popup" let:marker>
-        {@const { lat, lon } = marker}
-        <div class="flex flex-col items-center gap-1">
-          <p class="font-bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</p>
-          <a
-            href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=15#map=15/{lat}/{lon}"
-            target="_blank"
-            class="font-medium text-immich-primary"
-          >
-            Open in OpenStreetMap
-          </a>
+    {#await import('../shared-components/map/map.svelte')}
+      {#await delay(timeToLoadTheMap) then}
+        <!-- show the loading spinner only if loading the map takes too much time -->
+        <div class="flex items-center justify-center h-full w-full">
+          <LoadingSpinner />
         </div>
-      </svelte:fragment>
-    </Map>
+      {/await}
+    {:then component}
+      <svelte:component
+        this={component.default}
+        mapMarkers={[{ lat: latlng.lat, lon: latlng.lng, id: asset.id }]}
+        center={latlng}
+        zoom={15}
+        simplified
+        useLocationPin
+      >
+        <svelte:fragment slot="popup" let:marker>
+          {@const { lat, lon } = marker}
+          <div class="flex flex-col items-center gap-1">
+            <p class="font-bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</p>
+            <a
+              href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=15#map=15/{lat}/{lon}"
+              target="_blank"
+              class="font-medium text-immich-primary"
+            >
+              Open in OpenStreetMap
+            </a>
+          </div>
+        </svelte:fragment>
+      </svelte:component>
+    {/await}
   </div>
 {/if}
 
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index 397c167d16..ae99bafb0f 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -168,14 +168,22 @@
           class:hover:opacity-70={previousMemory}
         >
           <button class="relative h-full w-full rounded-2xl" disabled={!previousMemory} on:click={toPreviousMemory}>
-            <img
-              class="h-full w-full rounded-2xl object-cover"
-              src={previousMemory
-                ? getAssetThumbnailUrl(previousMemory.assets[0].id, ThumbnailFormat.Jpeg)
-                : noThumbnailUrl}
-              alt=""
-              draggable="false"
-            />
+            {#if previousMemory}
+              <img
+                class="h-full w-full rounded-2xl object-cover"
+                src={getAssetThumbnailUrl(previousMemory.assets[0].id, ThumbnailFormat.Jpeg)}
+                alt=""
+                draggable="false"
+              />
+            {:else}
+              <enhanced:img
+                class="h-full w-full rounded-2xl object-cover"
+                src={noThumbnailUrl}
+                sizes="min(271px,186px)"
+                alt=""
+                draggable="false"
+              />
+            {/if}
 
             {#if previousMemory}
               <div class="absolute bottom-4 right-4 text-left text-white">
@@ -233,12 +241,22 @@
           class:hover:opacity-70={nextMemory}
         >
           <button class="relative h-full w-full rounded-2xl" on:click={toNextMemory} disabled={!nextMemory}>
-            <img
-              class="h-full w-full rounded-2xl object-cover"
-              src={nextMemory ? getAssetThumbnailUrl(nextMemory.assets[0].id, ThumbnailFormat.Jpeg) : noThumbnailUrl}
-              alt=""
-              draggable="false"
-            />
+            {#if nextMemory}
+              <img
+                class="h-full w-full rounded-2xl object-cover"
+                src={getAssetThumbnailUrl(nextMemory.assets[0].id, ThumbnailFormat.Jpeg)}
+                alt=""
+                draggable="false"
+              />
+            {:else}
+              <enhanced:img
+                class="h-full w-full rounded-2xl object-cover"
+                src={noThumbnailUrl}
+                sizes="min(271px,186px)"
+                alt=""
+                draggable="false"
+              />
+            {/if}
 
             {#if nextMemory}
               <div class="absolute bottom-4 left-4 text-left text-white">
diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte
index a8fbe3a020..2e95c94f99 100644
--- a/web/src/lib/components/shared-components/change-location.svelte
+++ b/web/src/lib/components/shared-components/change-location.svelte
@@ -2,7 +2,10 @@
   import type { AssetResponseDto } from '@immich/sdk';
   import { createEventDispatcher } from 'svelte';
   import ConfirmDialogue from './confirm-dialogue.svelte';
-  import Map from './map/map.svelte';
+  import LoadingSpinner from './loading-spinner.svelte';
+  import { delay } from '$lib/utils/asset-utils';
+  import { timeToLoadTheMap } from '$lib/constants';
+
   export const title = 'Change Location';
   export let asset: AssetResponseDto | undefined = undefined;
 
@@ -48,14 +51,24 @@
   <div slot="prompt" class="flex flex-col w-full h-full gap-2">
     <label for="datetime">Pick a location</label>
     <div class="h-[500px] min-h-[300px] w-full">
-      <Map
-        mapMarkers={lat && lng && asset ? [{ id: asset.id, lat, lon: lng }] : []}
-        {zoom}
-        center={lat && lng ? { lat, lng } : undefined}
-        simplified={true}
-        clickable={true}
-        on:clickedPoint={({ detail: point }) => handleSelect(point)}
-      />
+      {#await import('../shared-components/map/map.svelte')}
+        {#await delay(timeToLoadTheMap) then}
+          <!-- show the loading spinner only if loading the map takes too much time -->
+          <div class="flex items-center justify-center h-full w-full">
+            <LoadingSpinner />
+          </div>
+        {/await}
+      {:then component}
+        <svelte:component
+          this={component.default}
+          mapMarkers={lat && lng && asset ? [{ id: asset.id, lat, lon: lng }] : []}
+          {zoom}
+          center={lat && lng ? { lat, lng } : undefined}
+          simplified={true}
+          clickable={true}
+          on:clickedPoint={({ detail: point }) => handleSelect(point)}
+        />
+      {/await}
     </div>
   </div>
 </ConfirmDialogue>
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
index 207a15bfc9..719b4791a7 100644
--- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
+++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
@@ -85,7 +85,7 @@
         />
       {/await}
     {:else}
-      <img
+      <enhanced:img
         src={noThumbnailUrl}
         alt={'Album without assets'}
         class="h-[100px] w-[100px] rounded-lg object-cover"
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index baf6628276..239df2f844 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -86,7 +86,11 @@ export enum ActionQueryParameterValue {
 
 export const maximumLengthSearchPeople: number = 20;
 
+// time to load the map before displaying the loading spinner
+export const timeToLoadTheMap: number = 100;
+
 export const timeBeforeShowLoadingSpinner: number = 100;
+
 // should be the same values as the ones in the app.html
 export enum Theme {
   LIGHT = 'light',
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 6120bbcfcc..ce56fa6e4b 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -226,3 +226,7 @@ export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserRespo
   }
   return ids;
 };
+
+export const delay = async (ms: number) => {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+};
diff --git a/web/vite.config.js b/web/vite.config.js
index b946622f1e..a2e69f77b6 100644
--- a/web/vite.config.js
+++ b/web/vite.config.js
@@ -1,3 +1,4 @@
+import { enhancedImages } from '@sveltejs/enhanced-img';
 import { sveltekit } from '@sveltejs/kit/vite';
 import path from 'node:path';
 import { visualizer } from 'rollup-plugin-visualizer';
@@ -33,6 +34,7 @@ export default defineConfig({
       emitFile: true,
       filename: 'stats.html',
     }),
+    enhancedImages(),
   ],
   optimizeDeps: {
     entries: ['src/**/*.{svelte,ts,html}'],