diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md
index 9777d00262..03c1a7a02b 100644
--- a/docs/docs/guides/template-backup-script.md
+++ b/docs/docs/guides/template-backup-script.md
@@ -78,4 +78,4 @@ borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint
 cd /tmp/immich-mountpoint
 ```
 
-You can find available snapshots in seperate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
+You can find available snapshots in separate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts
index 222f76be63..700ae0c373 100644
--- a/web/src/lib/actions/intersection-observer.ts
+++ b/web/src/lib/actions/intersection-observer.ts
@@ -10,10 +10,10 @@ type TrackedProperties = {
   left?: string;
 };
 type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElement) => unknown;
-type OnSeperateCallback = (element: HTMLElement) => unknown;
+type OnSeparateCallback = (element: HTMLElement) => unknown;
 type IntersectionObserverActionProperties = {
   key?: string;
-  onSeparate?: OnSeperateCallback;
+  onSeparate?: OnSeparateCallback;
   onIntersect?: OnIntersectCallback;
 
   root?: Element | Document | null;
@@ -22,8 +22,6 @@ type IntersectionObserverActionProperties = {
   right?: string;
   bottom?: string;
   left?: string;
-
-  disabled?: boolean;
 };
 type TaskKey = HTMLElement | string;
 
@@ -92,11 +90,7 @@ function _intersectionObserver(
   element: HTMLElement,
   properties: IntersectionObserverActionProperties,
 ) {
-  if (properties.disabled) {
-    properties.onIntersect?.(element);
-  } else {
-    configure(key, element, properties);
-  }
+  configure(key, element, properties);
   return {
     update(properties: IntersectionObserverActionProperties) {
       const config = elementToConfig.get(key);
@@ -106,20 +100,14 @@ function _intersectionObserver(
       if (isEquivalent(config, properties)) {
         return;
       }
+
       configure(key, element, properties);
     },
     destroy: () => {
-      if (properties.disabled) {
-        properties.onSeparate?.(element);
-      } else {
-        const config = elementToConfig.get(key);
-        const { observer, onSeparate } = config || {};
-        observer?.unobserve(element);
-        elementToConfig.delete(key);
-        if (onSeparate) {
-          onSeparate?.(element);
-        }
-      }
+      const config = elementToConfig.get(key);
+      const { observer } = config || {};
+      observer?.unobserve(element);
+      elementToConfig.delete(key);
     },
   };
 }
@@ -148,5 +136,5 @@ export function intersectionObserver(
       },
     };
   }
-  return _intersectionObserver(element, element, properties);
+  return _intersectionObserver(properties.key || element, element, properties);
 }
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 5cbc2e7dca..240b6c2ba2 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -35,7 +35,7 @@
   $: dateGroups = bucket.dateGroups;
 
   const {
-    DATEGROUP: { INTERSECTION_DISABLED, INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM },
+    DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM },
   } = TUNABLES;
   /* TODO figure out a way to calculate this*/
   const TITLE_HEIGHT = 51;
@@ -116,7 +116,6 @@
         top: INTERSECTION_ROOT_TOP,
         bottom: INTERSECTION_ROOT_BOTTOM,
         root: assetGridElement,
-        disabled: INTERSECTION_DISABLED,
       }}
       data-display={display}
       data-date-group={dateGroup.date}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index 94e7803b97..f59911dbaf 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -804,12 +804,13 @@
     class:invisible={showSkeleton}
     style:height={$assetStore.timelineHeight + 'px'}
   >
-    {#each $assetStore.buckets as bucket (bucket.bucketDate)}
+    {#each $assetStore.buckets as bucket (bucket.viewId)}
       {@const isPremeasure = preMeasure.includes(bucket)}
       {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure}
       <div
         id="bucket"
         use:intersectionObserver={{
+          key: bucket.viewId,
           onIntersect: () => handleIntersect(bucket),
           onSeparate: () => handleSeparate(bucket),
           top: BUCKET_INTERSECTION_ROOT_TOP,
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts
index 763d5b1874..ed6f9fdf04 100644
--- a/web/src/lib/stores/assets.store.ts
+++ b/web/src/lib/stores/assets.store.ts
@@ -2,6 +2,7 @@ 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 { AssetGridRouteSearchParams } from '$lib/utils/navigation';
 import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util';
 import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
@@ -12,7 +13,6 @@ import { t } from 'svelte-i18n';
 import { get, writable, type Unsubscriber } from 'svelte/store';
 import { handleError } from '../utils/handle-error';
 import { websocketEvents } from './websocket';
-
 type AssetApiGetTimeBucketsRequest = Parameters<typeof getTimeBuckets>[0];
 export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'>;
 
@@ -70,7 +70,10 @@ export class AssetBucket {
     Object.assign(this, props);
     this.init();
   }
-
+  /** The svelte key for this view model object */
+  get viewId() {
+    return this.store.viewId + '-' + this.bucketDate;
+  }
   private init() {
     // create a promise, and store its resolve/reject callbacks. The loadedSignal callback
     // will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal
@@ -205,21 +208,23 @@ export class AssetStore {
   private assetToBucket: Record<string, AssetLookup> = {};
   private pendingChanges: PendingChange[] = [];
   private unsubscribers: Unsubscriber[] = [];
-  private options: AssetApiGetTimeBucketsRequest;
+  private options!: AssetApiGetTimeBucketsRequest;
   private viewport: Viewport = {
     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;
   /**
    * A promise that resolves once the store is initialized.
    */
-  taskManager = new AssetGridTaskManager(this);
   complete!: Promise<void>;
+  taskManager = new AssetGridTaskManager(this);
   initialized = false;
   timelineHeight = 0;
   buckets: AssetBucket[] = [];
@@ -234,13 +239,23 @@ export class AssetStore {
     options: AssetStoreOptions,
     private albumId?: string,
   ) {
+    this.setOptions(options);
+    this.createInitializationSignal();
+    this.store$.set(this);
+  }
+
+  private setOptions(options: AssetStoreOptions) {
     this.options = { ...options, size: TimeBucketSize.Month };
+  }
+
+  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.initializedSignal = resolve;
     });
-    this.store$.set(this);
+    //  uncaught rejection go away
+    this.complete.catch(() => void 0);
   }
 
   private addPendingChanges(...changes: PendingChange[]) {
@@ -273,6 +288,7 @@ export class AssetStore {
     for (const unsubscribe of this.unsubscribers) {
       unsubscribe();
     }
+    this.unsubscribers = [];
   }
 
   private getPendingChangeBatches() {
@@ -360,8 +376,10 @@ export class AssetStore {
     if (bucketListener) {
       this.addListener(bucketListener);
     }
-    //  uncaught rejection go away
-    this.complete.catch(() => void 0);
+    await this.initialiazeTimeBuckets();
+  }
+
+  async initialiazeTimeBuckets() {
     this.timelineHeight = 0;
     this.buckets = [];
     this.assets = [];
@@ -379,6 +397,27 @@ export class AssetStore {
     this.initialized = true;
   }
 
+  async updateOptions(options: AssetStoreOptions) {
+    if (!this.initialized) {
+      this.setOptions(options);
+      return;
+    }
+    // TODO: don't call updateObjects frequently after
+    // init - cancelation of the initialize tasks isn't
+    // performed right now, and will cause issues if
+    // multiple updateOptions() calls are interleved.
+    await this.complete;
+    this.taskManager.destroy();
+    this.taskManager = new AssetGridTaskManager(this);
+    this.initialized = false;
+    this.viewId = generateId();
+    this.createInitializationSignal();
+    this.setOptions(options);
+    await this.initialiazeTimeBuckets();
+    this.emit(true);
+    await this.initialLayout(true);
+  }
+
   public destroy() {
     this.taskManager.destroy();
     this.listeners = [];
@@ -386,22 +425,21 @@ export class AssetStore {
   }
 
   async updateViewport(viewport: Viewport, force?: boolean) {
-    if (!this.initialized) {
-      return;
-    }
     if (viewport.height === 0 && viewport.width === 0) {
       return;
     }
-
     if (!force && this.viewport.height === viewport.height && this.viewport.width === viewport.width) {
       return;
     }
-
+    await this.complete;
     // changing width invalidates the actual height, and needs to be remeasured, since width changes causes
     // layout reflows.
     const changedWidth = this.viewport.width != viewport.width;
     this.viewport = { ...viewport };
+    await this.initialLayout(changedWidth);
+  }
 
+  private async initialLayout(changedWidth: boolean) {
     for (const bucket of this.buckets) {
       this.updateGeometry(bucket, changedWidth);
     }
@@ -410,7 +448,7 @@ export class AssetStore {
     const loaders = [];
     let height = 0;
     for (const bucket of this.buckets) {
-      if (height >= viewport.height) {
+      if (height >= this.viewport.height) {
         break;
       }
       height += bucket.bucketHeight;
diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts
index e476738456..60004235f4 100644
--- a/web/src/lib/utils/asset-store-task-manager.ts
+++ b/web/src/lib/utils/asset-store-task-manager.ts
@@ -315,7 +315,7 @@ class IntersectionTask {
     return { task: execTask, cleanup };
   }
 
-  trackSeperatedTask(componentId: string, task: Task) {
+  trackSeparatedTask(componentId: string, task: Task) {
     const execTask = () => {
       if (this.intersected) {
         return;
@@ -363,7 +363,7 @@ class IntersectionTask {
       return;
     }
 
-    const { task, cleanup } = this.trackSeperatedTask(componentId, separated);
+    const { task, cleanup } = this.trackSeparatedTask(componentId, separated);
     this.internalTaskManager.queueSeparateTask({
       task,
       cleanup,
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 a5d3630aa0..ce91abb451 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
@@ -40,10 +40,16 @@
     return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
   };
 
+  const assetStore = new AssetStore({});
+
   $: tags = data.tags;
   $: tagsMap = buildMap(tags);
   $: tag = currentPath ? tagsMap[currentPath] : null;
+  $: tagId = tag?.id;
   $: tree = buildTree(tags.map((tag) => tag.value));
+  $: {
+    void assetStore.updateOptions({ tagId });
+  }
 
   const handleNavigation = async (tag: string) => {
     await navigateToView(normalizeTreePath(`${data.path || ''}/${tag}`));
@@ -169,20 +175,13 @@
   <Breadcrumbs {pathSegments} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
 
   <section class="mt-2 h-full">
-    {#key $page.url.href}
-      {#if tag}
-        <AssetGrid
-          enableRouting={true}
-          assetStore={new AssetStore({ tagId: tag.id })}
-          {assetInteractionStore}
-          removeAction={AssetAction.UNARCHIVE}
-        >
-          <TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} slot="empty" />
-        </AssetGrid>
-      {:else}
-        <TreeItemThumbnails items={Object.keys(tree)} icon={mdiTag} onClick={handleNavigation} />
-      {/if}
-    {/key}
+    {#if tag}
+      <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
+        <TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} slot="empty" />
+      </AssetGrid>
+    {:else}
+      <TreeItemThumbnails items={Object.keys(tree)} icon={mdiTag} onClick={handleNavigation} />
+    {/if}
   </section>
 </UserPageLayout>