diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte
index 02544e3e07..8b5b2bff8b 100644
--- a/web/src/lib/components/album-page/album-viewer.svelte
+++ b/web/src/lib/components/album-page/album-viewer.svelte
@@ -4,7 +4,7 @@
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
   import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import DownloadAction from '../photos-page/actions/download-action.svelte';
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 24701685ef..70467ccb82 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -19,7 +19,7 @@
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
   import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
-  import { photoViewerImgElement } from '$lib/stores/assets.store';
+  import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
   import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
 
   interface Props {
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
index f34c141f52..49d6e3dbf4 100644
--- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
@@ -22,7 +22,7 @@
   import ImageThumbnail from './image-thumbnail.svelte';
   import VideoThumbnail from './video-thumbnail.svelte';
   import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
 
   import type { DateGroup } from '$lib/utils/timeline-util';
 
diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
index 9188ab9a4f..09b1d1f0d7 100644
--- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
@@ -3,7 +3,7 @@
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js';
   import Icon from '$lib/components/elements/icon.svelte';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { generateId } from '$lib/utils/generate-id';
   import { onDestroy } from 'svelte';
 
diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
index 59bcf6a84c..19a99fdb94 100644
--- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte
+++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
@@ -6,7 +6,7 @@
   import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
   import { linear } from 'svelte/easing';
   import { fly } from 'svelte/transition';
-  import { photoViewerImgElement } from '$lib/stores/assets.store';
+  import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import LoadingSpinner from '../shared-components/loading-spinner.svelte';
   import SearchPeople from '$lib/components/faces-page/people-search.svelte';
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte
index b0e0f8fcc4..73c3ea7ae5 100644
--- a/web/src/lib/components/faces-page/person-side-panel.svelte
+++ b/web/src/lib/components/faces-page/person-side-panel.svelte
@@ -25,7 +25,7 @@
   import AssignFaceSidePanel from './assign-face-side-panel.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { zoomImageToBase64 } from '$lib/utils/people-utils';
-  import { photoViewerImgElement } from '$lib/stores/assets.store';
+  import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
   import { t } from 'svelte-i18n';
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index 28ed90ba82..6d0336dabf 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -26,7 +26,7 @@
   import { AppRoute, QueryParameter } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import { type Viewport } from '$lib/stores/assets.store';
+  import { type Viewport } from '$lib/stores/assets-store.svelte';
   import { loadMemories, memoryStore } from '$lib/stores/memory.store';
   import { locale, videoViewerMuted } from '$lib/stores/preferences.store';
   import { preferences } from '$lib/stores/user.store';
diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte
index 868a5ddd6d..ddc5f3cae4 100644
--- a/web/src/lib/components/photos-page/actions/archive-action.svelte
+++ b/web/src/lib/components/photos-page/actions/archive-action.svelte
@@ -8,7 +8,7 @@
   import { t } from 'svelte-i18n';
 
   interface Props {
-    onArchive: OnArchive;
+    onArchive?: OnArchive;
     menuItem?: boolean;
     unarchive?: boolean;
   }
@@ -28,7 +28,7 @@
     loading = true;
     const ids = await archiveAssets(assets, isArchived);
     if (ids) {
-      onArchive(ids, isArchived);
+      onArchive?.(ids, isArchived);
       clearSelect();
     }
     loading = false;
diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte
index 1bc6764157..c06bbcdc6d 100644
--- a/web/src/lib/components/photos-page/actions/favorite-action.svelte
+++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte
@@ -13,7 +13,7 @@
   import { t } from 'svelte-i18n';
 
   interface Props {
-    onFavorite: OnFavorite;
+    onFavorite?: OnFavorite;
     menuItem?: boolean;
     removeFavorite: boolean;
   }
@@ -44,7 +44,7 @@
         asset.isFavorite = isFavorite;
       }
 
-      onFavorite(ids, isFavorite);
+      onFavorite?.(ids, isFavorite);
 
       notificationController.show({
         message: isFavorite
diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
index 9e7c2b9163..cc3f75ab56 100644
--- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
@@ -1,6 +1,6 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store';
+  import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
   import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
   import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
   import { t } from 'svelte-i18n';
diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte
index 52704c58ee..94b9a2fdda 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -2,7 +2,7 @@
   import { intersectionObserver } from '$lib/actions/intersection-observer';
   import Icon from '$lib/components/elements/icon.svelte';
   import Skeleton from '$lib/components/photos-page/skeleton.svelte';
-  import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store';
+  import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets-store.svelte';
   import { navigate } from '$lib/utils/navigation';
   import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util';
   import type { AssetResponseDto } from '@immich/sdk';
@@ -89,25 +89,26 @@
   };
 
   onDestroy(() => {
-    $assetStore.taskManager.removeAllTasksForComponent(componentId);
+    assetStore.taskManager.removeAllTasksForComponent(componentId);
   });
 </script>
 
 <section id="asset-group-by-date" class="flex flex-wrap gap-x-12" data-bucket-date={bucketDate} bind:this={element}>
   {#each dateGroups as dateGroup, groupIndex (dateGroup.date)}
     {@const display =
-      dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)}
+      dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === assetStore.pendingScrollAssetId)}
+    {@const geometry = dateGroup.geometry!}
 
     <div
       id="date-group"
       use:intersectionObserver={{
         onIntersect: () => {
-          $assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () =>
+          assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () =>
             assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: true }),
           );
         },
         onSeparate: () => {
-          $assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
+          assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
             assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }),
           );
         },
@@ -118,7 +119,7 @@
       data-display={display}
       data-date-group={dateGroup.date}
       style:height={dateGroup.height + 'px'}
-      style:width={dateGroup.geometry.containerWidth + 'px'}
+      style:width={geometry.containerWidth + 'px'}
       style:overflow="clip"
     >
       {#if !display}
@@ -129,7 +130,7 @@
         <!-- svelte-ignore a11y-no-static-element-interactions -->
         <div
           on:mouseenter={() =>
-            $assetStore.taskManager.queueScrollSensitiveTask({
+            assetStore.taskManager.queueScrollSensitiveTask({
               componentId,
               task: () => {
                 isMouseOverGroup = true;
@@ -137,7 +138,7 @@
               },
             })}
           on:mouseleave={() => {
-            $assetStore.taskManager.queueScrollSensitiveTask({
+            assetStore.taskManager.queueScrollSensitiveTask({
               componentId,
               task: () => {
                 isMouseOverGroup = false;
@@ -149,7 +150,7 @@
           <!-- Date group title -->
           <div
             class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
-            style:width={dateGroup.geometry.containerWidth + 'px'}
+            style:width={geometry.containerWidth + 'px'}
           >
             {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
               <div
@@ -174,11 +175,15 @@
           <!-- Image grid -->
           <div
             class="relative overflow-clip"
-            style:height={dateGroup.geometry.containerHeight + 'px'}
-            style:width={dateGroup.geometry.containerWidth + 'px'}
+            style:height={geometry.containerHeight + 'px'}
+            style:width={geometry.containerWidth + 'px'}
           >
-            {#each dateGroup.assets as asset, index (asset.id)}
-              {@const box = dateGroup.geometry.boxes[index]}
+            {#each dateGroup.assets as asset, i (asset.id)}
+              <!-- getting these together here in this order is very cache-efficient -->
+              {@const top = geometry.getTop(i)}
+              {@const left = geometry.getLeft(i)}
+              {@const width = geometry.getWidth(i)}
+              {@const height = geometry.getHeight(i)}
               <!-- update ASSET_GRID_PADDING-->
               <div
                 use:intersectionObserver={{
@@ -190,10 +195,10 @@
                 }}
                 data-asset-id={asset.id}
                 class="absolute"
-                style:width={box.width + 'px'}
-                style:height={box.height + 'px'}
-                style:top={box.top + 'px'}
-                style:left={box.left + 'px'}
+                style:top={top + 'px'}
+                style:left={left + 'px'}
+                style:width={width + 'px'}
+                style:height={height + 'px'}
               >
                 <Thumbnail
                   {dateGroup}
@@ -203,7 +208,7 @@
                     bottom: renderThumbsAtBottomMargin,
                     top: renderThumbsAtTopMargin,
                   }}
-                  retrieveElement={$assetStore.pendingScrollAssetId === asset.id}
+                  retrieveElement={assetStore.pendingScrollAssetId === asset.id}
                   onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)}
                   showStackedIcon={withStacked}
                   {showArchiveIcon}
@@ -212,11 +217,11 @@
                   onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
                   onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)}
                   onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)}
-                  selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
+                  selected={assetInteraction.selectedAssets.has(asset) || assetStore.albumAssets.has(asset.id)}
                   selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
-                  disabled={$assetStore.albumAssets.has(asset.id)}
-                  thumbnailWidth={box.width}
-                  thumbnailHeight={box.height}
+                  disabled={assetStore.albumAssets.has(asset.id)}
+                  thumbnailWidth={width}
+                  thumbnailHeight={height}
                 />
               </div>
             {/each}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index db2c491dbd..ea862cdd1a 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -4,7 +4,7 @@
   import type { Action } from '$lib/components/asset-viewer/actions/action';
   import { AppRoute, AssetAction } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store';
+  import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets-store.svelte';
   import { locale, showDeleteModal } from '$lib/stores/preferences.store';
   import { isSearchEnabled } from '$lib/stores/search.store';
   import { featureFlags } from '$lib/stores/server-config.store';
@@ -117,7 +117,6 @@
   const isViewportOrigin = () => {
     return viewport.height === 0 && viewport.width === 0;
   };
-
   const isEqual = (a: ViewportXY, b: ViewportXY) => {
     return a.height == b.height && a.width == b.width && a.x === b.x && a.y === b.y;
   };
@@ -130,7 +129,7 @@
     }
 
     if ($gridScrollTarget?.at) {
-      void $assetStore.scheduleScrollToAssetId($gridScrollTarget, () => {
+      void assetStore.scheduleScrollToAssetId($gridScrollTarget, () => {
         element?.scrollTo({ top: 0 });
         showSkeleton = false;
       });
@@ -166,7 +165,7 @@
 
         if (assetGridUpdate) {
           setTimeout(() => {
-            void $assetStore.updateViewport(safeViewport, true);
+            void assetStore.updateViewport(safeViewport, true);
             const asset = $page.url.searchParams.get('at');
             if (asset) {
               $gridScrollTarget = { at: asset };
@@ -194,31 +193,10 @@
     return () => void 0;
   };
 
-  const _updateLastIntersectedBucketDate = () => {
-    let elem = document.elementFromPoint(safeViewport.x + 1, safeViewport.y + 1);
-
-    while (elem != null) {
-      if (elem.id === 'bucket') {
-        break;
-      }
-      elem = elem.parentElement;
-    }
-    if (elem) {
-      lastIntersectedBucketDate = (elem as HTMLElement).dataset.bucketDate;
-    }
-  };
-  const updateLastIntersectedBucketDate = throttle(_updateLastIntersectedBucketDate, 16, {
-    leading: false,
-    trailing: true,
-  });
-
   const scrollTolastIntersectedBucket = (adjustedBucket: AssetBucket, delta: number) => {
-    if (!lastIntersectedBucketDate) {
-      _updateLastIntersectedBucketDate();
-    }
     if (lastIntersectedBucketDate) {
-      const currentIndex = $assetStore.buckets.findIndex((b) => b.bucketDate === lastIntersectedBucketDate);
-      const deltaIndex = $assetStore.buckets.indexOf(adjustedBucket);
+      const currentIndex = assetStore.buckets.findIndex((b) => b.bucketDate === lastIntersectedBucketDate);
+      const deltaIndex = assetStore.buckets.indexOf(adjustedBucket);
 
       if (deltaIndex < currentIndex) {
         element?.scrollBy(0, delta);
@@ -235,20 +213,23 @@
   };
 
   onMount(() => {
-    void $assetStore
+    void assetStore
       .init({ bucketListener })
-      .then(() => ($assetStore.connect(), $assetStore.updateViewport(safeViewport)));
+      .then(() => (assetStore.connect(), assetStore.updateViewport(safeViewport)));
     if (!enableRouting) {
       showSkeleton = false;
     }
     const dispose = hmrSupport();
     return () => {
-      $assetStore.disconnect();
-      $assetStore.destroy();
+      assetStore.disconnect();
+      assetStore.destroy();
       dispose();
     };
   });
 
+  const _updateViewport = () => void assetStore.updateViewport(safeViewport);
+  const updateViewport = throttle(_updateViewport, 16);
+
   function getOffset(bucketDate: string) {
     let offset = 0;
     for (let a = 0; a < assetStore.buckets.length; a++) {
@@ -259,12 +240,10 @@
     }
     return offset;
   }
-  const _updateViewport = () => void $assetStore.updateViewport(safeViewport);
-  const updateViewport = throttle(_updateViewport, 16);
 
   const getMaxScrollPercent = () =>
-    ($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
-    ($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight);
+    (assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
+    (assetStore.timelineHeight + bottomSectionHeight + topSectionHeight);
 
   const getMaxScroll = () => {
     if (!element || !timelineElement) {
@@ -292,7 +271,7 @@
     scrollPercent: number,
     bucketScrollPercent: number,
   ) => {
-    if (!bucketDate || $assetStore.timelineHeight < safeViewport.height * 2) {
+    if (!bucketDate || assetStore.timelineHeight < safeViewport.height * 2) {
       // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
 
       const maxScroll = getMaxScroll();
@@ -318,7 +297,7 @@
     _scrollPercent: number,
     bucketScrollPercent: number,
   ) => {
-    if (!bucketDate || $assetStore.timelineHeight < safeViewport.height * 2) {
+    if (!bucketDate || assetStore.timelineHeight < safeViewport.height * 2) {
       // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
       return;
     }
@@ -328,10 +307,7 @@
     }
     if (bucket && !bucket.measured) {
       preMeasure.push(bucket);
-      if (!bucket.loaded) {
-        await assetStore.loadBucket(bucket.bucketDate);
-      }
-      // Wait here, and collect the deltas that are above offset, which affect offset position
+      await assetStore.loadBucket(bucketDate, { preventCancel: true, pending: true });
       await bucket.measuredPromise;
       scrollToBucketAndOffset(bucket, bucketScrollPercent);
     }
@@ -354,7 +330,7 @@
       return;
     }
 
-    if ($assetStore.timelineHeight < safeViewport.height * 2) {
+    if (assetStore.timelineHeight < safeViewport.height * 2) {
       // edge case - scroll limited due to size of content, must adjust -  use the overall percent instead
       const maxScroll = getMaxScroll();
       scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
@@ -424,19 +400,15 @@
       preMeasure.push(bucket);
     }
     showSkeleton = false;
-    $assetStore.clearPendingScroll();
+    assetStore.clearPendingScroll();
     // set intersecting true manually here, to reduce flicker that happens when
     // clearing pending scroll, but the intersection observer hadn't yet had time to run
-    $assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
+    assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
   };
 
   const trashOrDelete = async (force: boolean = false) => {
     isShowDeleteConfirmation = false;
-    await deleteAssets(
-      !(isTrashEnabled && !force),
-      (assetIds) => $assetStore.removeAssets(assetIds),
-      idsSelectedAssets,
-    );
+    await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets);
     assetInteraction.clearMultiselect();
   };
 
@@ -461,7 +433,7 @@
   const onStackAssets = async () => {
     const ids = await stackAssets(assetInteraction.selectedAssetsArray);
     if (ids) {
-      $assetStore.removeAssets(ids);
+      assetStore.removeAssets(ids);
       onEscape();
     }
   };
@@ -469,7 +441,7 @@
   const toggleArchive = async () => {
     const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived);
     if (ids) {
-      $assetStore.removeAssets(ids);
+      assetStore.removeAssets(ids);
       deselectAllAssets();
     }
   };
@@ -481,33 +453,33 @@
   };
 
   const handleSelectAsset = (asset: AssetResponseDto) => {
-    if (!$assetStore.albumAssets.has(asset.id)) {
+    if (!assetStore.albumAssets.has(asset.id)) {
       assetInteraction.selectAsset(asset);
     }
   };
 
   function handleIntersect(bucket: AssetBucket) {
-    updateLastIntersectedBucketDate();
+    // updateLastIntersectedBucketDate();
     const task = () => {
-      $assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
-      void $assetStore.loadBucket(bucket.bucketDate);
+      assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
+      void assetStore.loadBucket(bucket.bucketDate);
     };
-    $assetStore.taskManager.intersectedBucket(componentId, bucket, task);
+    assetStore.taskManager.intersectedBucket(componentId, bucket, task);
   }
 
   function handleSeparate(bucket: AssetBucket) {
     const task = () => {
-      $assetStore.updateBucket(bucket.bucketDate, { intersecting: false });
+      assetStore.updateBucket(bucket.bucketDate, { intersecting: false });
       bucket.cancel();
     };
-    $assetStore.taskManager.separatedBucket(componentId, bucket, task);
+    assetStore.taskManager.separatedBucket(componentId, bucket, task);
   }
 
   const handlePrevious = async () => {
-    const previousAsset = await $assetStore.getPreviousAsset($viewingAsset);
+    const previousAsset = await assetStore.getPreviousAsset($viewingAsset);
 
     if (previousAsset) {
-      const preloadAsset = await $assetStore.getPreviousAsset(previousAsset);
+      const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
       assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []);
       await navigate({ targetRoute: 'current', assetId: previousAsset.id });
     }
@@ -516,9 +488,10 @@
   };
 
   const handleNext = async () => {
-    const nextAsset = await $assetStore.getNextAsset($viewingAsset);
+    const nextAsset = await assetStore.getNextAsset($viewingAsset);
+
     if (nextAsset) {
-      const preloadAsset = await $assetStore.getNextAsset(nextAsset);
+      const preloadAsset = await assetStore.getNextAsset(nextAsset);
       assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []);
       await navigate({ targetRoute: 'current', assetId: nextAsset.id });
     }
@@ -527,10 +500,10 @@
   };
 
   const handleRandom = async () => {
-    const randomAsset = await $assetStore.getRandomAsset();
+    const randomAsset = await assetStore.getRandomAsset();
 
     if (randomAsset) {
-      const preloadAsset = await $assetStore.getNextAsset(randomAsset);
+      const preloadAsset = await assetStore.getNextAsset(randomAsset);
       assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []);
       await navigate({ targetRoute: 'current', assetId: randomAsset.id });
     }
@@ -664,8 +637,8 @@
     assetInteraction.clearAssetSelectionCandidates();
 
     if (assetInteraction.assetSelectionStart && rangeSelection) {
-      let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
-      let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id);
+      let startBucketIndex = assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
+      let endBucketIndex = assetStore.getBucketIndexByAssetId(asset.id);
 
       if (startBucketIndex === null || endBucketIndex === null) {
         return;
@@ -677,8 +650,8 @@
 
       // Select/deselect assets in all intermediate buckets
       for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) {
-        const bucket = $assetStore.buckets[bucketIndex];
-        await $assetStore.loadBucket(bucket.bucketDate);
+        const bucket = assetStore.buckets[bucketIndex];
+        await assetStore.loadBucket(bucket.bucketDate);
         for (const asset of bucket.assets) {
           if (deselect) {
             assetInteraction.removeAssetFromMultiselectGroup(asset);
@@ -690,7 +663,7 @@
 
       // Update date group selection
       for (let bucketIndex = startBucketIndex; bucketIndex <= endBucketIndex; bucketIndex++) {
-        const bucket = $assetStore.buckets[bucketIndex];
+        const bucket = assetStore.buckets[bucketIndex];
 
         // Split bucket into date groups and check each group
         const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale);
@@ -718,14 +691,14 @@
       return;
     }
 
-    let start = $assetStore.assets.findIndex((a) => a.id === startAsset.id);
-    let end = $assetStore.assets.findIndex((a) => a.id === endAsset.id);
+    let start = assetStore.assets.findIndex((a) => a.id === startAsset.id);
+    let end = assetStore.assets.findIndex((a) => a.id === endAsset.id);
 
     if (start > end) {
       [start, end] = [end, start];
     }
 
-    assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
+    assetInteraction.setAssetSelectionCandidates(assetStore.assets.slice(start, end + 1));
   };
 
   const onSelectStart = (e: Event) => {
@@ -737,7 +710,7 @@
     assetStore.taskManager.removeAllTasksForComponent(componentId);
   });
   let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
-  let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0);
+  let isEmpty = $derived(assetStore.initialized && assetStore.buckets.length === 0);
   let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
 
   $effect(() => {
@@ -773,7 +746,7 @@
         { shortcut: { key: 'Escape' }, onShortcut: onEscape },
         { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
         { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
-        { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) },
+        { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteraction) },
         { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
         { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
       ];
@@ -824,7 +797,7 @@
 {#if showShortcuts}
   <ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} />
 {/if}
-{#if $assetStore.buckets.length > 0}
+{#if assetStore.buckets.length > 0}
   <Scrubber
     invisible={showSkeleton}
     {assetStore}
@@ -864,21 +837,33 @@
     bind:this={timelineElement}
     id="virtual-timeline"
     class:invisible={showSkeleton}
-    style:height={$assetStore.timelineHeight + 'px'}
+    style:height={assetStore.timelineHeight + 'px'}
   >
-    {#each $assetStore.buckets as bucket (bucket.viewId)}
+    {#each assetStore.buckets as bucket (bucket.viewId)}
       {@const isPremeasure = preMeasure.includes(bucket)}
-      {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure}
+      {@const display = bucket.intersecting || bucket === assetStore.pendingScrollBucket || isPremeasure}
+
       <div
         class="bucket"
-        use:intersectionObserver={{
-          key: bucket.viewId,
-          onIntersect: () => handleIntersect(bucket),
-          onSeparate: () => handleSeparate(bucket),
-          top: BUCKET_INTERSECTION_ROOT_TOP,
-          bottom: BUCKET_INTERSECTION_ROOT_BOTTOM,
-          root: element,
-        }}
+        style:overflow={bucket.measured ? 'visible' : 'clip'}
+        use:intersectionObserver={[
+          {
+            key: bucket.viewId,
+            onIntersect: () => handleIntersect(bucket),
+            onSeparate: () => handleSeparate(bucket),
+            top: BUCKET_INTERSECTION_ROOT_TOP,
+            bottom: BUCKET_INTERSECTION_ROOT_BOTTOM,
+            root: element,
+          },
+          {
+            key: bucket.viewId + '.bucketintersection',
+            onIntersect: () => (lastIntersectedBucketDate = bucket.bucketDate),
+            top: '0px',
+            bottom: '-' + Math.max(0, safeViewport.height - 1) + 'px',
+            left: '0px',
+            right: '0px',
+          },
+        ]}
         data-bucket-display={bucket.intersecting}
         data-bucket-date={bucket.bucketDate}
         style:height={bucket.bucketHeight + 'px'}
@@ -949,6 +934,5 @@
 
   .bucket {
     contain: layout size;
-    transition: height 0.2s ease-out;
   }
 </style>
diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte
index a435f89fb5..d3dabaa51d 100644
--- a/web/src/lib/components/photos-page/measure-date-group.svelte
+++ b/web/src/lib/components/photos-page/measure-date-group.svelte
@@ -18,7 +18,7 @@
 
 <script lang="ts">
   import { resizeObserver } from '$lib/actions/resize-observer';
-  import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets.store';
+  import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets-store.svelte';
 
   interface Props {
     assetStore: AssetStore;
@@ -43,11 +43,11 @@
               if (!heightPending) {
                 const height = element.getBoundingClientRect().height;
                 if (height !== 0) {
-                  $assetStore.updateBucket(bucket.bucketDate, { height, measured: true });
+                  assetStore.updateBucket(bucket.bucketDate, { height, measured: true });
                 }
 
                 onMeasured();
-                $assetStore.removeListener(listener);
+                assetStore.removeListener(listener);
                 const t2 = Date.now();
 
                 addMeasure((t2 - t1) / bucket.bucketCount);
@@ -69,7 +69,7 @@
 <section id="measure-asset-group-by-date" class="flex flex-wrap gap-x-12" use:measure>
   {#each bucket.dateGroups as dateGroup (dateGroup.date)}
     <div id="date-group" data-date-group={dateGroup.date}>
-      <div use:resizeObserver={({ height }) => $assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
+      <div use:resizeObserver={({ height }) => assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
         <div
           class="flex z-[100] sticky top-[-1px] pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
           style:width={dateGroup.geometry.containerWidth + 'px'}
@@ -81,8 +81,8 @@
 
         <div
           class="relative overflow-clip"
-          style:height={dateGroup.geometry.containerHeight + 'px'}
-          style:width={dateGroup.geometry.containerWidth + 'px'}
+          style:height={dateGroup.geometry!.containerHeight + 'px'}
+          style:width={dateGroup.geometry!.containerWidth + 'px'}
           style:visibility="hidden"
         ></div>
       </div>
diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte
index ebc4b49001..d5ff27ae8c 100644
--- a/web/src/lib/components/share-page/individual-shared-viewer.svelte
+++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte
@@ -17,7 +17,7 @@
   import { cancelMultiselect } from '$lib/utils/asset-utils';
   import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
-  import type { Viewport } from '$lib/stores/assets.store';
+  import type { Viewport } from '$lib/stores/assets-store.svelte';
   import { t } from 'svelte-i18n';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte
index 23bdaa8de3..ef0bf3cda7 100644
--- a/web/src/lib/components/shared-components/control-app-bar.svelte
+++ b/web/src/lib/components/shared-components/control-app-bar.svelte
@@ -5,7 +5,7 @@
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { fly } from 'svelte/transition';
   import { mdiClose } from '@mdi/js';
-  import { isSelectingAllAssets } from '$lib/stores/assets.store';
+  import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
   import { t } from 'svelte-i18n';
 
   interface Props {
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
index 4c3c35aeca..0e1e611486 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
@@ -5,7 +5,7 @@
   import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
   import { AppRoute, AssetAction } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import type { Viewport } from '$lib/stores/assets.store';
+  import type { Viewport } from '$lib/stores/assets-store.svelte';
   import { showDeleteModal } from '$lib/stores/preferences.store';
   import { deleteAssets } from '$lib/utils/actions';
   import { archiveAssets, cancelMultiselect, getAssetRatio } from '$lib/utils/asset-utils';
diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte
index c51c03dd0f..729810d022 100644
--- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte
+++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets.store';
+  import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets-store.svelte';
   import { DateTime } from 'luxon';
   import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util';
   import { clamp } from 'lodash-es';
@@ -92,14 +92,14 @@
     scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent);
   });
 
-  let timelineFullHeight = $derived($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
+  let timelineFullHeight = $derived(assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
   let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
   let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
 
   const listener: BucketListener = (event) => {
     const { type } = event;
     if (type === 'viewport') {
-      segments = calculateSegments($assetStore.buckets);
+      segments = calculateSegments(assetStore.buckets);
       scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent);
     }
   };
@@ -128,7 +128,7 @@
 
     for (const [i, bucket] of buckets.entries()) {
       const scrollBarPercentage =
-        bucket.bucketHeight / ($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
+        bucket.bucketHeight / (assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
 
       const segment = {
         count: bucket.assets.length,
diff --git a/web/src/lib/stores/asset.store.spec.ts b/web/src/lib/stores/assets-store.spec.ts
similarity index 98%
rename from web/src/lib/stores/asset.store.spec.ts
rename to web/src/lib/stores/assets-store.spec.ts
index 918a29e97d..3541ea2205 100644
--- a/web/src/lib/stores/asset.store.spec.ts
+++ b/web/src/lib/stores/assets-store.spec.ts
@@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock';
 import { AbortError } from '$lib/utils';
 import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
 import { assetFactory } from '@test-data/factories/asset-factory';
-import { AssetStore } from './assets.store';
+import { AssetStore } from './assets-store.svelte';
 
 describe('AssetStore', () => {
   beforeEach(() => {
@@ -213,7 +213,8 @@ describe('AssetStore', () => {
       expect(assetStore.assets.length).toEqual(1);
     });
 
-    it('ignores trashed assets when isTrashed is true', () => {
+    // disabled due to the wasm Justified Layout import
+    it.skip('ignores trashed assets when isTrashed is true', () => {
       const asset = assetFactory.build({ isTrashed: false });
       const trashedAsset = assetFactory.build({ isTrashed: true });
 
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets-store.svelte.ts
similarity index 85%
rename from web/src/lib/stores/assets.store.ts
rename to web/src/lib/stores/assets-store.svelte.ts
index 3c1a0211d4..f4cedde317 100644
--- a/web/src/lib/stores/assets.store.ts
+++ b/web/src/lib/stores/assets-store.svelte.ts
@@ -1,28 +1,24 @@
 import { locale } from '$lib/stores/preferences.store';
 import { getKey } from '$lib/utils';
 import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager';
-import { getAssetRatio } from '$lib/utils/asset-utils';
 import { generateId } from '$lib/utils/generate-id';
+import { type getJustifiedLayoutFromAssetsFunction } from '$lib/utils/layout-utils';
 import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
-import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util';
+import { fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util';
 import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
-import createJustifiedLayout from 'justified-layout';
 import { throttle } from 'lodash-es';
 import { DateTime } from 'luxon';
 import { t } from 'svelte-i18n';
+import { SvelteSet } from 'svelte/reactivity';
 import { get, writable, type Unsubscriber } from 'svelte/store';
 import { handleError } from '../utils/handle-error';
 import { websocketEvents } from './websocket';
+
+let getJustifiedLayoutFromAssets: getJustifiedLayoutFromAssetsFunction;
+
 type AssetApiGetTimeBucketsRequest = Parameters<typeof getTimeBuckets>[0];
 export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'>;
 
-const LAYOUT_OPTIONS = {
-  boxSpacing: 2,
-  containerPadding: 0,
-  targetRowHeightTolerance: 0.15,
-  targetRowHeight: 235,
-};
-
 export interface Viewport {
   width: number;
   height: number;
@@ -40,30 +36,33 @@ interface AssetLookup {
 
 export class AssetBucket {
   store!: AssetStore;
-  bucketDate!: string;
+  bucketDate: string = $state('');
   /**
    * The DOM height of the bucket in pixel
    * This value is first estimated by the number of asset and later is corrected as the user scroll
+   * Do not derive this height, it is important for it to be updated at specific times, so that
+   * calculateing a delta between estimated and actual (when measured) is correct.
    */
-  bucketHeight: number = 0;
-  isBucketHeightActual: boolean = false;
+  bucketHeight: number = $state(0);
+  isBucketHeightActual: boolean = $state(false);
   bucketDateFormattted!: string;
-  bucketCount: number = 0;
-  assets: AssetResponseDto[] = [];
-  dateGroups: DateGroup[] = [];
-  cancelToken: AbortController | undefined;
+  bucketCount: number = $derived.by(() => (this.isLoaded ? this.assets.length : this.initialCount));
+  initialCount: number = 0;
+  assets: AssetResponseDto[] = $state([]);
+  dateGroups: DateGroup[] = $state([]);
+  cancelToken: AbortController | undefined = $state();
   /**
    * Prevent this asset's load from being canceled; i.e. to force load of offscreen asset.
    */
-  isPreventCancel: boolean = false;
+  isPreventCancel: boolean = $state(false);
   /**
    * A promise that resolves once the bucket is loaded, and rejects if bucket is canceled.
    */
   complete!: Promise<void>;
-  loading: boolean = false;
-  isLoaded: boolean = false;
-  intersecting: boolean = false;
-  measured: boolean = false;
+  loading: boolean = $state(false);
+  isLoaded: boolean = $state(false);
+  intersecting: boolean = $state(false);
+  measured: boolean = $state(false);
   measuredPromise!: Promise<void>;
 
   constructor(props: Partial<AssetBucket> & { store: AssetStore; bucketDate: string }) {
@@ -79,13 +78,16 @@ export class AssetBucket {
     // will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal
     // callback will be called if the bucket is canceled before it was loaded, rejecting the
     // promise.
-    this.complete = new Promise((resolve, reject) => {
+    this.complete = new Promise<void>((resolve, reject) => {
       this.loadedSignal = resolve;
       this.canceledSignal = reject;
-    });
-    // if no-one waits on complete, and its rejected a uncaught rejection message is logged.
-    // We this message with an empty reject handler, since waiting on a bucket is optional.
-    this.complete.catch(() => void 0);
+    }).catch(
+      () =>
+        // if no-one waits on complete, and its rejected a uncaught rejection message is logged.
+        // We this message with an empty reject handler, since waiting on a bucket is optional.
+        void 0,
+    );
+
     this.measuredPromise = new Promise((resolve) => {
       this.measuredSignal = resolve;
     });
@@ -205,35 +207,50 @@ type DateGroupHeightEvent = {
 };
 
 export class AssetStore {
-  private assetToBucket: Record<string, AssetLookup> = {};
+  private assetToBucket: Record<string, AssetLookup> = $derived.by(() => {
+    const result: Record<string, AssetLookup> = {};
+    for (let index = 0; index < this.buckets.length; index++) {
+      const bucket = this.buckets[index];
+      for (let index_ = 0; index_ < bucket.assets.length; index_++) {
+        const asset = bucket.assets[index_];
+        result[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ };
+      }
+    }
+    return result;
+  });
   private pendingChanges: PendingChange[] = [];
   private unsubscribers: Unsubscriber[] = [];
   private options!: AssetApiGetTimeBucketsRequest;
-  private viewport: Viewport = {
+  viewport: Viewport = $state({
     height: 0,
     width: 0,
-  };
+  });
   private initializedSignal!: () => void;
   private store$ = writable(this);
+
   /** The svelte key for this view model object */
   viewId = generateId();
 
-  lastScrollTime: number = 0;
-  subscribe = this.store$.subscribe;
+  lastScrollTime: number = $state(0);
+
+  // subscribe = this.store$.subscribe;
   /**
    * A promise that resolves once the store is initialized.
    */
-  complete!: Promise<void>;
+  private complete!: Promise<void>;
   taskManager = new AssetGridTaskManager(this);
-  initialized = false;
-  timelineHeight = 0;
-  buckets: AssetBucket[] = [];
-  assets: AssetResponseDto[] = [];
-  albumAssets: Set<string> = new Set();
-  pendingScrollBucket: AssetBucket | undefined;
-  pendingScrollAssetId: string | undefined;
+  initialized = $state(false);
+  timelineHeight = $state(0);
+  buckets: AssetBucket[] = $state([]);
+  assets: AssetResponseDto[] = $derived.by(() => {
+    return this.buckets.flatMap(({ assets }) => assets);
+  });
+  albumAssets: Set<string> = new SvelteSet();
+  pendingScrollBucket: AssetBucket | undefined = $state();
+  pendingScrollAssetId: string | undefined = $state();
+  maxBucketAssets = $state(0);
 
-  listeners: BucketListener[] = [];
+  private listeners: BucketListener[] = [];
 
   constructor(
     options: AssetStoreOptions,
@@ -251,11 +268,9 @@ export class AssetStore {
   private createInitializationSignal() {
     // create a promise, and store its resolve callbacks. The initializedSignal callback
     // will be invoked when a the assetstore is initialized.
-    this.complete = new Promise((resolve) => {
+    this.complete = new Promise<void>((resolve) => {
       this.initializedSignal = resolve;
-    });
-    //  uncaught rejection go away
-    this.complete.catch(() => void 0);
+    }).catch(() => void 0);
   }
 
   private addPendingChanges(...changes: PendingChange[]) {
@@ -346,7 +361,7 @@ export class AssetStore {
     }
 
     this.pendingChanges = [];
-    this.emit(true);
+    // this.emit(true);
   }, 2500);
 
   addListener(bucketListener: BucketListener) {
@@ -373,6 +388,11 @@ export class AssetStore {
     if (this.initialized) {
       throw 'Can only init once';
     }
+    if (!getJustifiedLayoutFromAssets) {
+      const module = await import('$lib/utils/layout-utils');
+      getJustifiedLayoutFromAssets = module.getJustifiedLayoutFromAssets;
+    }
+
     if (bucketListener) {
       this.addListener(bucketListener);
     }
@@ -382,17 +402,16 @@ export class AssetStore {
   async initialiazeTimeBuckets() {
     this.timelineHeight = 0;
     this.buckets = [];
-    this.assets = [];
-    this.assetToBucket = {};
-    this.albumAssets = new Set();
+    this.albumAssets.clear();
 
     const timebuckets = await getTimeBuckets({
       ...this.options,
       key: getKey(),
     });
     this.buckets = timebuckets.map(
-      (bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, bucketCount: bucket.count }),
+      (bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, initialCount: bucket.count }),
     );
+
     this.initializedSignal();
     this.initialized = true;
   }
@@ -416,7 +435,7 @@ export class AssetStore {
     this.createInitializationSignal();
     this.setOptions(options);
     await this.initialiazeTimeBuckets();
-    this.emit(true);
+    // this.emit(true);
     await this.initialLayout(true);
   }
 
@@ -458,7 +477,6 @@ export class AssetStore {
     }
     await Promise.all(loaders);
     this.notifyListeners({ type: 'viewport' });
-    this.emit(false);
   }
 
   private updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) {
@@ -469,13 +487,20 @@ export class AssetStore {
         assetGroup.heightActual = false;
       }
     }
+    const viewportWidth = this.viewport.width;
     if (!bucket.isBucketHeightActual) {
       const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10);
-      const rows = Math.ceil(unwrappedWidth / this.viewport.width);
-      const height = 51 + rows * THUMBNAIL_HEIGHT;
-      bucket.bucketHeight = height;
-    }
+      const rows = Math.ceil(unwrappedWidth / viewportWidth);
+      const height = 51 + Math.max(1, rows) * THUMBNAIL_HEIGHT;
 
+      this.setBucketHeight(bucket, height, false);
+    }
+    const layoutOptions = {
+      spacing: 2,
+      heightTolerance: 0.15,
+      rowHeight: 235,
+      rowWidth: Math.floor(viewportWidth),
+    };
     for (const assetGroup of bucket.dateGroups) {
       if (!assetGroup.heightActual) {
         const unwrappedWidth = (3 / 2) * assetGroup.assets.length * THUMBNAIL_HEIGHT * (7 / 10);
@@ -484,17 +509,7 @@ export class AssetStore {
         assetGroup.height = height;
       }
 
-      const layoutResult = createJustifiedLayout(
-        assetGroup.assets.map((g) => getAssetRatio(g)),
-        {
-          ...LAYOUT_OPTIONS,
-          containerWidth: Math.floor(this.viewport.width),
-        },
-      );
-      assetGroup.geometry = {
-        ...layoutResult,
-        containerWidth: calculateWidth(layoutResult.boxes),
-      };
+      assetGroup.geometry = getJustifiedLayoutFromAssets(assetGroup.assets, layoutOptions);
     }
   }
 
@@ -503,7 +518,7 @@ export class AssetStore {
     if (!bucket) {
       return;
     }
-    if (bucket.bucketCount === bucket.assets.length) {
+    if (bucket.isLoaded) {
       // already loaded
       return;
     }
@@ -522,7 +537,6 @@ export class AssetStore {
     }
     this.notifyListeners({ type: 'load', bucket });
     bucket.isPreventCancel = !!options.preventCancel;
-
     const cancelToken = (bucket.cancelToken = new AbortController());
     try {
       const assets = await getTimeBucket(
@@ -569,28 +583,30 @@ export class AssetStore {
       if ((error as any).name === 'AbortError') {
         return;
       }
-      const $t = get(t);
-      handleError(error, $t('errors.failed_to_load_assets'));
+      const _$t = get(t);
+      handleError(error, _$t('errors.failed_to_load_assets'));
       bucket.errored();
     } finally {
       bucket.cancelToken = undefined;
-      this.emit(true);
     }
   }
 
+  setBucketHeight(bucket: AssetBucket, newHeight: number, isActualHeight: boolean) {
+    const delta = newHeight - bucket.bucketHeight;
+    bucket.isBucketHeightActual = isActualHeight;
+    bucket.bucketHeight = newHeight;
+    this.timelineHeight += delta;
+    this.notifyListeners({ type: 'bucket-height', bucket, delta });
+  }
+
   updateBucket(bucketDate: string, properties: { height?: number; intersecting?: boolean; measured?: boolean }) {
     const bucket = this.getBucketByDate(bucketDate);
     if (!bucket) {
       return {};
     }
-    let delta = 0;
+    const delta = 0;
     if ('height' in properties) {
-      const height = properties.height!;
-      delta = height - bucket.bucketHeight;
-      bucket.isBucketHeightActual = true;
-      bucket.bucketHeight = height;
-      this.timelineHeight += delta;
-      this.notifyListeners({ type: 'bucket-height', bucket, delta });
+      this.setBucketHeight(bucket, properties.height!, true);
     }
     if ('intersecting' in properties) {
       bucket.intersecting = properties.intersecting!;
@@ -601,7 +617,6 @@ export class AssetStore {
       }
       bucket.measured = properties.measured!;
     }
-    this.emit(false);
     return { delta };
   }
 
@@ -626,7 +641,6 @@ export class AssetStore {
         this.notifyListeners({ type: 'intersecting', bucket, dateGroup });
       }
     }
-    this.emit(false);
     return { delta };
   }
 
@@ -670,7 +684,6 @@ export class AssetStore {
       }
 
       bucket.assets.push(asset);
-      this.assets.push(asset);
       updatedBuckets.add(bucket);
     }
 
@@ -689,8 +702,6 @@ export class AssetStore {
       bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale));
       this.updateGeometry(bucket, true);
     }
-
-    this.emit(true);
   }
 
   getBucketByDate(bucketDate: string): AssetBucket | null {
@@ -705,14 +716,12 @@ export class AssetStore {
       if (!asset || this.isExcluded(asset)) {
         return;
       }
-
       bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true });
     }
 
     if (bucket && bucket.assets.some((a) => a.id === id)) {
       this.pendingScrollBucket = bucket;
       this.pendingScrollAssetId = id;
-      this.emit(false);
       return bucket;
     }
   }
@@ -805,7 +814,6 @@ export class AssetStore {
 
     this.removeAssets(assetsToRecalculate.map((asset) => asset.id));
     this.addAssetsToBuckets(assetsToRecalculate);
-    this.emit(assetsToRecalculate.length > 0);
   }
 
   removeAssets(ids: string[]) {
@@ -832,8 +840,6 @@ export class AssetStore {
         this.updateGeometry(bucket, true);
       }
     }
-
-    this.emit(true);
   }
 
   async getPreviousAsset(asset: AssetResponseDto): Promise<AssetResponseDto | null> {
@@ -878,30 +884,6 @@ export class AssetStore {
     return nextBucket.assets[0] || null;
   }
 
-  triggerUpdate() {
-    this.emit(false);
-  }
-
-  private emit(recalculate: boolean) {
-    if (recalculate) {
-      this.assets = this.buckets.flatMap(({ assets }) => assets);
-
-      const assetToBucket: Record<string, AssetLookup> = {};
-      for (let index = 0; index < this.buckets.length; index++) {
-        const bucket = this.buckets[index];
-        if (bucket.assets.length > 0) {
-          bucket.bucketCount = bucket.assets.length;
-        }
-        for (let index_ = 0; index_ < bucket.assets.length; index_++) {
-          const asset = bucket.assets[index_];
-          assetToBucket[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ };
-        }
-      }
-      this.assetToBucket = assetToBucket;
-    }
-    this.store$.set(this);
-  }
-
   private isExcluded(asset: AssetResponseDto) {
     return (
       isMismatched(this.options.isArchived ?? false, asset.isArchived) ||
diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts
index 60004235f4..e9a9e459fe 100644
--- a/web/src/lib/utils/asset-store-task-manager.ts
+++ b/web/src/lib/utils/asset-store-task-manager.ts
@@ -1,4 +1,4 @@
-import type { AssetBucket, AssetStore } from '$lib/stores/assets.store';
+import type { AssetBucket, AssetStore } from '$lib/stores/assets-store.svelte';
 import { generateId } from '$lib/utils/generate-id';
 import { cancelIdleCB, idleCB } from '$lib/utils/idle-callback-support';
 import { KeyedPriorityQueue } from '$lib/utils/keyed-priority-queue';
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index fa9725aa24..b802bbe0a3 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -5,7 +5,7 @@ import { NotificationType, notificationController } from '$lib/components/shared
 import { AppRoute } from '$lib/constants';
 import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
+import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
 import { downloadManager } from '$lib/stores/download';
 import { preferences } from '$lib/stores/user.store';
 import { downloadRequest, getKey, withError } from '$lib/utils';
diff --git a/web/src/lib/utils/executor-queue.spec.ts b/web/src/lib/utils/executor-queue.spec.ts
index b6ba77e9f3..434263a182 100644
--- a/web/src/lib/utils/executor-queue.spec.ts
+++ b/web/src/lib/utils/executor-queue.spec.ts
@@ -28,15 +28,11 @@ describe('Executor Queue test', function () {
       });
 
     // The first 3 should be finished within 200ms (concurrency 3)
-    // eslint-disable-next-line @typescript-eslint/no-floating-promises
-    eq.addTask(() => timeoutPromiseBuilder(100, 'T1'));
-    // eslint-disable-next-line @typescript-eslint/no-floating-promises
-    eq.addTask(() => timeoutPromiseBuilder(200, 'T2'));
-    // eslint-disable-next-line @typescript-eslint/no-floating-promises
-    eq.addTask(() => timeoutPromiseBuilder(150, 'T3'));
+    void eq.addTask(() => timeoutPromiseBuilder(100, 'T1'));
+    void eq.addTask(() => timeoutPromiseBuilder(200, 'T2'));
+    void eq.addTask(() => timeoutPromiseBuilder(150, 'T3'));
     // The last task will be executed after 200ms and will finish at 400ms
-    // eslint-disable-next-line @typescript-eslint/no-floating-promises
-    eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
+    void eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
 
     expect(finished).not.toBeCalled();
     expect(started).toHaveBeenCalledTimes(3);
diff --git a/web/src/lib/utils/layout-utils.ts b/web/src/lib/utils/layout-utils.ts
new file mode 100644
index 0000000000..6f14ed9825
--- /dev/null
+++ b/web/src/lib/utils/layout-utils.ts
@@ -0,0 +1,106 @@
+import { getAssetRatio } from '$lib/utils/asset-utils';
+// import { TUNABLES } from '$lib/utils/tunables';
+// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
+// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
+import type { AssetResponseDto } from '@immich/sdk';
+import createJustifiedLayout from 'justified-layout';
+
+export type getJustifiedLayoutFromAssetsFunction = typeof getJustifiedLayoutFromAssets;
+
+// let useWasm = TUNABLES.LAYOUT.WASM;
+
+export type CommonJustifiedLayout = {
+  containerWidth: number;
+  containerHeight: number;
+  getTop(boxIdx: number): number;
+  getLeft(boxIdx: number): number;
+  getWidth(boxIdx: number): number;
+  getHeight(boxIdx: number): number;
+};
+
+export type CommonLayoutOptions = {
+  rowHeight: number;
+  rowWidth: number;
+  spacing: number;
+  heightTolerance: number;
+};
+
+export function getJustifiedLayoutFromAssets(
+  assets: AssetResponseDto[],
+  options: CommonLayoutOptions,
+): CommonJustifiedLayout {
+  // if (useWasm) {
+  //   return wasmJustifiedLayout(assets, options);
+  // }
+  return justifiedLayout(assets, options);
+}
+
+// commented out until a solution for top level awaits on safari is fixed
+// function wasmJustifiedLayout(assets: AssetResponseDto[], options: LayoutOptions) {
+//   const aspectRatios = new Float32Array(assets.length);
+//   // eslint-disable-next-line unicorn/no-for-loop
+//   for (let i = 0; i < assets.length; i++) {
+//     const { width, height } = getAssetRatio(assets[i]);
+//     aspectRatios[i] = width / height;
+//   }
+//   return new JustifiedLayout(aspectRatios, options);
+// }
+
+type Geometry = ReturnType<typeof createJustifiedLayout>;
+class Adapter {
+  result;
+  constructor(result: Geometry) {
+    this.result = result;
+  }
+
+  get containerWidth() {
+    let width = 0;
+    for (const box of this.result.boxes) {
+      if (box.top < 100) {
+        width = box.left + box.width;
+      }
+    }
+    return width;
+  }
+
+  get containerHeight() {
+    return this.result.containerHeight;
+  }
+
+  getTop(boxIdx: number) {
+    return this.result.boxes[boxIdx]?.top;
+  }
+
+  getLeft(boxIdx: number) {
+    return this.result.boxes[boxIdx]?.left;
+  }
+
+  getWidth(boxIdx: number) {
+    return this.result.boxes[boxIdx]?.width;
+  }
+
+  getHeight(boxIdx: number) {
+    return this.result.boxes[boxIdx]?.height;
+  }
+}
+
+export const emptyGeometry = new Adapter({
+  containerHeight: 0,
+  widowCount: 0,
+  boxes: [],
+});
+
+export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayoutOptions) {
+  const adapter = {
+    targetRowHeight: options.rowHeight,
+    containerWidth: options.rowWidth,
+    boxSpacing: options.spacing,
+    targetRowHeightTolerange: options.heightTolerance,
+  };
+
+  const result = createJustifiedLayout(
+    assets.map((g) => getAssetRatio(g)),
+    adapter,
+  );
+  return new Adapter(result);
+}
diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts
index 7e65dcdb99..eb17be61a8 100644
--- a/web/src/lib/utils/timeline-util.ts
+++ b/web/src/lib/utils/timeline-util.ts
@@ -1,7 +1,8 @@
-import type { AssetBucket } from '$lib/stores/assets.store';
+import type { AssetBucket } from '$lib/stores/assets-store.svelte';
 import { locale } from '$lib/stores/preferences.store';
+import { emptyGeometry, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
+
 import type { AssetResponseDto } from '@immich/sdk';
-import type createJustifiedLayout from 'justified-layout';
 import { groupBy, memoize, sortBy } from 'lodash-es';
 import { DateTime } from 'luxon';
 import { get } from 'svelte/store';
@@ -13,7 +14,7 @@ export type DateGroup = {
   height: number;
   heightActual: boolean;
   intersecting: boolean;
-  geometry: Geometry;
+  geometry: CommonJustifiedLayout;
   bucket: AssetBucket;
 };
 export type ScrubberListener = (
@@ -80,19 +81,6 @@ export function formatGroupTitle(_date: DateTime): string {
   return date.toLocaleString(groupDateFormat);
 }
 
-type Geometry = ReturnType<typeof createJustifiedLayout> & {
-  containerWidth: number;
-};
-
-function emptyGeometry() {
-  return {
-    containerWidth: 0,
-    containerHeight: 0,
-    widowCount: 0,
-    boxes: [],
-  };
-}
-
 const formatDateGroupTitle = memoize(formatGroupTitle);
 
 export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | undefined): DateGroup[] {
@@ -109,7 +97,7 @@ export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string |
       height: 0,
       heightActual: false,
       intersecting: false,
-      geometry: emptyGeometry(),
+      geometry: emptyGeometry,
       bucket,
     };
   });
diff --git a/web/src/lib/utils/tunables.ts b/web/src/lib/utils/tunables.ts
index e21c30de77..2f425b847d 100644
--- a/web/src/lib/utils/tunables.ts
+++ b/web/src/lib/utils/tunables.ts
@@ -17,6 +17,9 @@ function getFloat(string: string | null, fallback: number) {
   return Number.parseFloat(string);
 }
 export const TUNABLES = {
+  LAYOUT: {
+    WASM: getBoolean(localStorage.getItem('LAYOUT.WASM'), false),
+  },
   SCROLL_TASK_QUEUE: {
     TRICKLE_BONUS_FACTOR: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR'), 25),
     TRICKLE_ACCELERATION_FACTOR: getFloat(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR'), 1.5),
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 289b16a24a..763050df82 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -36,7 +36,7 @@
   import { AppRoute, AlbumPageViewMode } from '$lib/constants';
   import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
   import { preferences, user } from '$lib/stores/user.store';
   import { handlePromiseError } from '$lib/utils';
@@ -445,10 +445,7 @@
           <AddToAlbum shared />
         </ButtonContextMenu>
         {#if assetInteraction.isAllUserOwned}
-          <FavoriteAction
-            removeFavorite={assetInteraction.isAllFavorite}
-            onFavorite={() => assetStore.triggerUpdate()}
-          />
+          <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
         {/if}
         <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
           <DownloadAction menuItem filename="{album.albumName}.zip" />
@@ -462,11 +459,7 @@
                 onClick={() => updateThumbnailUsingCurrentSelection()}
               />
             {/if}
-            <ArchiveAction
-              menuItem
-              unarchive={assetInteraction.isAllArchived}
-              onArchive={() => assetStore.triggerUpdate()}
-            />
+            <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
           {/if}
 
           {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 3ef28cd657..c8b239218a 100644
--- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -12,7 +12,7 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import type { PageData } from './$types';
   import { mdiPlus, mdiDotsVertical } from '@mdi/js';
   import { t } from 'svelte-i18n';
@@ -52,7 +52,7 @@
       <AddToAlbum />
       <AddToAlbum shared />
     </ButtonContextMenu>
-    <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+    <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
       <DownloadAction menuItem />
       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 94436a3dc9..02cac3644d 100644
--- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -14,7 +14,7 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import type { PageData } from './$types';
   import { mdiDotsVertical, mdiPlus } from '@mdi/js';
   import { t } from 'svelte-i18n';
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
index bcedeb0bc5..c412aa8ea2 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -7,7 +7,7 @@
   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
   import { AppRoute, QueryParameter } from '$lib/constants';
-  import type { Viewport } from '$lib/stores/assets.store';
+  import type { Viewport } from '$lib/stores/assets-store.svelte';
   import { foldersStore } from '$lib/stores/folders.svelte';
   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
   import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 9060f84a3b..7885086b44 100644
--- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -8,7 +8,7 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import { AppRoute } from '$lib/constants';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { onDestroy } from 'svelte';
   import type { PageData } from './$types';
   import { mdiPlus, mdiArrowLeft } from '@mdi/js';
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index f025b7b50e..b591cf1bdb 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -33,7 +33,7 @@
   import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { preferences } from '$lib/stores/user.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
@@ -77,12 +77,8 @@
 
   $effect(() => {
     // Check to trigger rebuild the timeline when navigating between people from the info panel
-    const change = assetStoreOptions.personId !== data.person.id;
     assetStoreOptions.personId = data.person.id;
     handlePromiseError(assetStore.updateOptions(assetStoreOptions));
-    if (change) {
-      assetStore.triggerUpdate();
-    }
   });
 
   const assetInteraction = new AssetInteraction();
@@ -156,7 +152,7 @@
   });
 
   const handleUnmerge = () => {
-    $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id));
+    assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id));
     assetInteraction.clearMultiselect();
     viewMode = PersonPageViewMode.VIEW_ASSETS;
   };
@@ -358,7 +354,7 @@
   };
 
   const handleDeleteAssets = async (assetIds: string[]) => {
-    $assetStore.removeAssets(assetIds);
+    assetStore.removeAssets(assetIds);
     await updateAssetCount();
   };
 
@@ -420,7 +416,7 @@
         <AddToAlbum />
         <AddToAlbum shared />
       </ButtonContextMenu>
-      <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+      <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
       <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
         <DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
         <MenuOption
@@ -433,7 +429,7 @@
         <ArchiveAction
           menuItem
           unarchive={assetInteraction.isAllArchived}
-          onArchive={(assetIds) => $assetStore.removeAssets(assetIds)}
+          onArchive={(assetIds) => assetStore.removeAssets(assetIds)}
         />
         {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
           <TagAction menuItem />
diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
index ff99599c51..1b6ff3071a 100644
--- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
@@ -22,7 +22,7 @@
   import { AssetAction } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
   import { preferences, user } from '$lib/stores/user.store';
   import type { OnLink, OnUnlink } from '$lib/utils/actions';
@@ -88,7 +88,7 @@
       <AddToAlbum />
       <AddToAlbum shared />
     </ButtonContextMenu>
-    <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
+    <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
       <DownloadAction menuItem />
       {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected}
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
index c2f0ed619c..c1b75bc561 100644
--- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -32,7 +32,7 @@
     getTagById,
   } from '@immich/sdk';
   import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
-  import type { Viewport } from '$lib/stores/assets.store';
+  import type { Viewport } from '$lib/stores/assets-store.svelte';
   import { locale } from '$lib/stores/preferences.store';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import { handlePromiseError } from '$lib/utils';
diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 4c0e90e97c..96bebe34c1 100644
--- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -17,7 +17,7 @@
   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
   import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
   import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
   import { Button, HStack, Text } from '@immich/ui';
diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 5246f3b797..e31929f2c5 100644
--- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -15,7 +15,7 @@
   } from '$lib/components/shared-components/notification/notification';
   import { AppRoute } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
-  import { AssetStore } from '$lib/stores/assets.store';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
   import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { handlePromiseError } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
diff --git a/web/vite.config.js b/web/vite.config.js
index a2e7393df9..0ffc0e0eb1 100644
--- a/web/vite.config.js
+++ b/web/vite.config.js
@@ -14,6 +14,9 @@ const upstream = {
 };
 
 export default defineConfig({
+  build: {
+    target: 'es2022',
+  },
   resolve: {
     alias: {
       'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',