diff --git a/common/constants.js b/common/constants.js
index 878b0f2bfdca4df5d9b3d261c5e682b1ad3079db..24f62981b7f7962f08ddaac2e453456ed5c7117b 100644
--- a/common/constants.js
+++ b/common/constants.js
@@ -37,4 +37,9 @@
     // additional volumes
     TOGGLE_SHOW_LAYER_CONTROL: `Show layer control`,
   }
+
+  exports.IDS = {
+    // mesh loading status
+    MESH_LOADING_STATUS: 'mesh-loading-status'
+  }
 })(typeof exports === 'undefined' ? module.exports : exports)
diff --git a/docs/releases/v2.3.0.md b/docs/releases/v2.3.0.md
index f1c252954124027ddae342d240177e93a357adc8..a4bf02816a303bd2a99582a30eb82172fb5fa1bc 100644
--- a/docs/releases/v2.3.0.md
+++ b/docs/releases/v2.3.0.md
@@ -9,7 +9,9 @@
 - Updated `README.md`
 - introduced zoom buttons
 - major UI overhaul
+- tweaked mesh loading strategies. Now it will wait for all image chunks to be loaded before loading any meshes
 
 # Bugfixes:
 
 - dataset list view explicitly show loading status
+
diff --git a/e2e/src/navigating/loadingOrder.prod.e2e-spec.js b/e2e/src/navigating/loadingOrder.prod.e2e-spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc62278d79d3ef39bf926952d4f8eec73e0a30cb
--- /dev/null
+++ b/e2e/src/navigating/loadingOrder.prod.e2e-spec.js
@@ -0,0 +1,120 @@
+const { AtlasPage } = require('../util')
+const { IDS } = require('../../../common/constants')
+const { width, height } = require('../../opts')
+const { hrtime } = process
+
+const templates = [
+  ["Big Brain (Histology)", "Cytoarchitectonic Maps"],
+  ["Waxholm Space rat brain MRI/DTI", "Waxholm Space rat brain atlas v3"]
+]
+
+describe('> when loading an atlas', () => {
+  let iavPage = new AtlasPage()
+
+  const getNumOfSpinners = async () => {
+
+    try {
+      return await iavPage.execScript(`
+        const els = document.querySelectorAll('.spinnerAnimationCircle')
+        return els && els.length
+        `)
+    } catch (e) {
+      return false
+    }
+  }
+
+  const checkRoleStatus = async () => {
+    try {
+      const text = await iavPage.execScript(`
+        const el = document.getElementById('${IDS.MESH_LOADING_STATUS}')
+        return el && el.textContent
+        `)
+      return text && /Loading\s.*?chunks/.test(text)
+    } catch (e) {
+      return false
+    }
+  }
+
+  beforeEach(async () => {
+    iavPage = new AtlasPage()
+    await iavPage.init()
+    await iavPage.goto()
+  })
+
+  for (const [template, parcelation] of templates) {
+
+    const masterTimer = {moving: null, nonmoving: null}
+
+    it(`> for ${template}, image will be loaded first`, async () => {
+      const timer = hrtime()
+      await iavPage.selectTitleTemplateParcellation(template, parcelation)
+      await iavPage.wait(500)
+      const preSpinners = await getNumOfSpinners()
+      expect(preSpinners).toBe(4)
+      while( await checkRoleStatus() ) {
+        await iavPage.wait(200)
+      }
+
+      const finishedTimer = hrtime(timer)
+
+      const postSpinners = await getNumOfSpinners()
+      /**
+       * There is currently a known bug that spinner in IDS.MESH_LOADING_STATUS is hidden with opacity 0 rather than unload from DOM
+       * TODO fix bug
+       */
+      expect(postSpinners).toBeLessThanOrEqual(2)
+      expect(postSpinners).toBeGreaterThanOrEqual(1)
+      
+      const sec = finishedTimer[0] + finishedTimer[1] / 1e9
+      masterTimer.nonmoving = sec
+    })
+
+    it('> if user was panning, it should take longer as loading takes priority', async () => {
+      const timer = hrtime()
+      await iavPage.selectTitleTemplateParcellation(template, parcelation)
+      await iavPage.wait(500)
+      
+      await iavPage.cursorMoveToAndDrag({
+        position: [ Math.round(width / 4 * 3), Math.round(height / 4) ],
+        delta: [ Math.round(width / 8), Math.round(height / 8)]
+      })
+
+      await iavPage.wait(500)
+
+      await iavPage.cursorMoveToAndDrag({
+        position: [ Math.round(width / 4 * 3), Math.round(height / 4) ],
+        delta: [ 1, Math.round(height / -4)]
+      })
+
+      while( await checkRoleStatus() ) {
+        await iavPage.wait(200)
+      }
+
+      const finishedTimer = hrtime(timer)
+      
+      const sec = finishedTimer[0] + finishedTimer[1] / 1e9
+      masterTimer.moving = sec
+
+      expect(masterTimer.moving).toBeGreaterThan(masterTimer.nonmoving)
+    })
+
+    it('> if the meshes are already loading, do not show overlay', async () => {
+      
+      await iavPage.selectTitleTemplateParcellation(template, parcelation)
+      await iavPage.wait(500)
+      
+      while( await checkRoleStatus() ) {
+        await iavPage.wait(200)
+      }
+
+      await iavPage.cursorMoveToAndDrag({
+        position: [ Math.round(width / 4 * 3), Math.round(height / 4) ],
+        delta: [ Math.round(width / 8), Math.round(height / 8)]
+      })
+
+      const roleStatus = await checkRoleStatus()
+
+      expect(roleStatus).toBeFalsy()
+    })
+  }
+})
diff --git a/e2e/src/util.js b/e2e/src/util.js
index 4fd6e1db3c93034063432025888360c55d641cd3..faa4c075089321477470dfcc4f6fa98490ab925c 100644
--- a/e2e/src/util.js
+++ b/e2e/src/util.js
@@ -235,17 +235,22 @@ class WdBase{
   async waitForAsync(){
 
     const checkReady = async () => {
-      const el = await this._browser.findElements(
+      const els = await this._browser.findElements(
         By.css('.spinnerAnimationCircle')
       )
-      return !el.length
+      const visibleEls = []
+      for (const el of els) {
+        if (await el.isDisplayed()) {
+          visibleEls.push(el)
+        }
+      }
+      return !visibleEls.length
     }
 
     do {
-      // Do nothing, until ready
+      await this.wait(500)
     } while (
-      await this.wait(100),
-      !(await checkReady())
+      !(await retry(checkReady.bind(this), { timeout: 1000, retries: 10 }))
     )
   }
 
diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts
index af96ee110dbff8e7447eed7e7290a97e87b098a6..ce664b8210b801a8ba204181aa93204706c0f087 100644
--- a/src/ui/nehubaContainer/nehubaContainer.component.ts
+++ b/src/ui/nehubaContainer/nehubaContainer.component.ts
@@ -1,6 +1,6 @@
 import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, ChangeDetectorRef, Output, EventEmitter } from "@angular/core";
 import { select, Store } from "@ngrx/store";
-import { combineLatest, fromEvent, merge, Observable, of, Subscription, timer } from "rxjs";
+import { combineLatest, fromEvent, merge, Observable, of, Subscription, timer, asyncScheduler } from "rxjs";
 import { pipeFromArray } from "rxjs/internal/util/pipe";
 import {
   buffer,
@@ -19,6 +19,7 @@ import {
   tap,
   withLatestFrom,
   delayWhen,
+  throttleTime,
 } from "rxjs/operators";
 import { LoggingService } from "src/logging";
 import { FOUR_PANEL, H_ONE_THREE, NG_VIEWER_ACTION_TYPES, SINGLE_PANEL, V_ONE_THREE } from "src/services/state/ngViewerState.store";
@@ -27,49 +28,20 @@ import { ADD_NG_LAYER, generateLabelIndexId, getMultiNgIdsRegionsLabelIndexMap,
 import { getExportNehuba, isSame } from "src/util/fn";
 import { AtlasViewerAPIServices, IUserLandmark } from "src/atlasViewer/atlasViewer.apiService.service";
 import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component";
-import { getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, calculateSliceZoomFactor } from "./util";
+import { getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, calculateSliceZoomFactor, scanSliceViewRenderFn as scanFn, isFirstRow, isFirstCell } from "./util";
 import { NehubaViewerContainerDirective } from "./nehubaViewerInterface/nehubaViewerInterface.directive";
 import { ITunableProp } from "./mobileOverlay/mobileOverlay.component";
 import { compareLandmarksChanged } from "src/util/constants";
 import { PureContantService } from "src/util";
-import { ARIA_LABELS } from 'common/constants'
+import { ARIA_LABELS, IDS } from 'common/constants'
+
+const { MESH_LOADING_STATUS } = IDS
 
 const { 
   ZOOM_IN,
   ZOOM_OUT,
 } = ARIA_LABELS
 
-const isFirstRow = (cell: HTMLElement) => {
-  const { parentElement: row } = cell
-  const { parentElement: container } = row
-  return container.firstElementChild === row
-}
-
-const isFirstCell = (cell: HTMLElement) => {
-  const { parentElement: row } = cell
-  return row.firstElementChild === cell
-}
-
-const scanFn: (acc: [boolean, boolean, boolean], curr: CustomEvent) => [boolean, boolean, boolean] = (acc, curr) => {
-
-  const target = curr.target as HTMLElement
-  const targetIsFirstRow = isFirstRow(target)
-  const targetIsFirstCell = isFirstCell(target)
-  const idx = targetIsFirstRow
-    ? targetIsFirstCell
-      ? 0
-      : 1
-    : targetIsFirstCell
-      ? 2
-      : null
-
-  const returnAcc = [...acc]
-  const num1 = typeof curr.detail.missingChunks === 'number' ? curr.detail.missingChunks : 0
-  const num2 = typeof curr.detail.missingImageChunks === 'number' ? curr.detail.missingImageChunks : 0
-  returnAcc[idx] = Math.max(num1, num2) > 0
-  return returnAcc as [boolean, boolean, boolean]
-}
-
 @Component({
   selector : 'ui-nehuba-container',
   templateUrl : './nehubaContainer.template.html',
@@ -83,6 +55,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
 
   public ARIA_LABEL_ZOOM_IN = ZOOM_IN
   public ARIA_LABEL_ZOOM_OUT = ZOOM_OUT
+  public ID_MESH_LOADING_STATUS = MESH_LOADING_STATUS
 
   @ViewChild(NehubaViewerContainerDirective,{static: true})
   public nehubaContainerDirective: NehubaViewerContainerDirective
@@ -97,8 +70,10 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
 
   public viewerLoaded: boolean = false
 
+  private sliceRenderEvent$: Observable<CustomEvent> 
   public sliceViewLoadingMain$: Observable<[boolean, boolean, boolean]>
   public perspectiveViewLoading$: Observable<string|null>
+  public showPerpsectiveScreen$: Observable<string>
 
   public templateSelected$: Observable<any>
   private newViewer$: Observable<any>
@@ -230,34 +205,69 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
       distinctUntilChanged(),
     )
 
-    this.sliceViewLoadingMain$ = fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent').pipe(
+    this.sliceRenderEvent$ = fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent').pipe(
+      shareReplay(1),
+      map(ev => ev as CustomEvent)
+    )
+
+    this.sliceViewLoadingMain$ = this.sliceRenderEvent$.pipe(
       scan(scanFn, [null, null, null]),
       startWith([true, true, true] as [boolean, boolean, boolean]),
       shareReplay(1),
     )
 
+    this.showPerpsectiveScreen$ = this.newViewer$.pipe(
+      switchMapTo(this.sliceRenderEvent$.pipe(
+        scan((acc, curr) => {
+
+          /**
+           * if at any point, all chunks have been loaded, always return loaded state
+           */
+          if (acc.every(v => v === 0)) return [0, 0, 0]
+          const { detail = {}, target } = curr || {}
+          const { missingChunks = -1, missingImageChunks = -1 } = detail
+          const idx = this.findPanelIndex(target as HTMLElement)
+          const returnAcc = [...acc]
+          if (idx >= 0) {
+            returnAcc[idx] = missingChunks + missingImageChunks
+          }
+          return returnAcc
+        }, [-1, -1, -1]),
+        map(arr => {
+          let sum = 0
+          let uncertain = false
+          for (const num of arr) {
+            if (num < 0) {
+              uncertain = true
+            } else {
+              sum += num
+            }
+          }
+          return sum > 0
+            ? `Loading ${sum}${uncertain ? '+' : ''} chunks ...`
+            : null
+        }),
+        distinctUntilChanged(),
+        startWith('Loading ...'),
+        throttleTime(100, asyncScheduler, { leading: true, trailing: true })
+      ))
+    ) 
+
     /* missing chunk perspective view */
     this.perspectiveViewLoading$ = fromEvent(this.elementRef.nativeElement, 'perpspectiveRenderEvent')
       .pipe(
         filter(event => isDefined(event) && isDefined((event as any).detail) && isDefined((event as any).detail.lastLoadedMeshId) ),
         map(event => {
 
-          const e = (event as any)
-          const lastLoadedIdString = e.detail.lastLoadedMeshId.split(',')[0]
-          const lastLoadedIdNum = Number(lastLoadedIdString)
           /**
            * TODO dig into event detail to see if the exact mesh loaded
            */
-          return e.detail.meshesLoaded >= this.nehubaViewer.numMeshesToBeLoaded
+          const { meshesLoaded, meshFragmentsLoaded, lastLoadedMeshId } = (event as any).detail
+          return meshesLoaded >= this.nehubaViewer.numMeshesToBeLoaded
             ? null
-            : isNaN(lastLoadedIdNum)
-              ? 'Loading unknown chunk'
-              : lastLoadedIdNum >= 65500
-                ? 'Loading auxiliary chunk'
-                // : this.regionsLabelIndexMap.get(lastLoadedIdNum)
-                //   ? `Loading ${this.regionsLabelIndexMap.get(lastLoadedIdNum).name}`
-                : 'Loading meshes ...'
+            : 'Loading meshes ...'
         }),
+        distinctUntilChanged()
       )
 
     this.ngLayers$ = this.store.pipe(
@@ -308,7 +318,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
     /* each time a new viewer is initialised, take the first event to get the translation function */
     this.subscriptions.push(
       this.newViewer$.pipe(
-        switchMap(() => pipeFromArray([...takeOnePipe])(fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent'))),
+        switchMap(() => pipeFromArray([...takeOnePipe])(this.sliceRenderEvent$)),
       ).subscribe((events) => {
         for (const idx in [0, 1, 2]) {
           const ev = events[idx] as CustomEvent
diff --git a/src/ui/nehubaContainer/nehubaContainer.style.css b/src/ui/nehubaContainer/nehubaContainer.style.css
index dbb79cdf0a18e1c6d4d788e15662b3c15c35ca69..a3a28715e6e18ae84d41dba8115525a6c5568bb9 100644
--- a/src/ui/nehubaContainer/nehubaContainer.style.css
+++ b/src/ui/nehubaContainer/nehubaContainer.style.css
@@ -170,3 +170,13 @@ div#scratch-pad
   opacity: 1.0 !important;
   pointer-events: all !important;
 }
+
+.screen-overlay
+{
+  background-color: rgba(255, 255, 255, 0.7);
+}
+
+:host-context([darktheme="true"]) .screen-overlay
+{
+  background-color: rgba(0, 0, 0, 0.7);
+}
diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html
index f338d47990e31c4468de71b5e77375a73d15d645..bdaedfe4394a0b9d986616ad624373faff96119c 100644
--- a/src/ui/nehubaContainer/nehubaContainer.template.html
+++ b/src/ui/nehubaContainer/nehubaContainer.template.html
@@ -86,16 +86,32 @@
 
 <!-- perspective view tmpl -->
 <ng-template #overlayPerspectiveTmpl>
-  <layout-floating-container landmarkContainer>
+  <layout-floating-container class="tmp" landmarkContainer>
+
+    <div class="d-flex flex-column justify-content-center align-items-center w-100 h-100 position-absolute opacity-crossfade screen-overlay pe-none"
+      [ngClass]="{onHover: !!(showPerpsectiveScreen$ | async)}"
+      [attr.id]="ID_MESH_LOADING_STATUS"
+      role="status">
+
+      <div class="spinnerAnimationCircle">
+      </div>
+      <mat-list>
+        <mat-list-item>
+          {{ showPerpsectiveScreen$ | async }}
+        </mat-list-item>
+      </mat-list>
+    </div>
 
     <!-- maximise/minimise button -->
     <ng-container *ngTemplateOutlet="panelCtrlTmpl; context: { panelIndex: 3, visible: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async  )) === 3 }">
     </ng-container>
     
+    <!-- mesh loading is still weird -->
+    <!-- if the precomputed server does not have the necessary fragment file, then the numberws will not collate -->
     <div *ngIf="perspectiveViewLoading$ | async" class="loadingIndicator">
       <div class="spinnerAnimationCircle"></div>
 
-      <div perspectiveLoadingText>
+      <div *ngIf="false" perspectiveLoadingText>
         {{ perspectiveViewLoading$ | async }}
       </div>
     </div>
diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts
index 34904988db844bd741a769ce2e185fd109f7c4e8..035fb084662906c801ae9b11d2724cec4f50067f 100644
--- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts
+++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts
@@ -1,7 +1,7 @@
 import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Renderer2, Optional, Inject } from "@angular/core";
-import { fromEvent, Subscription, ReplaySubject, Subject, BehaviorSubject } from 'rxjs'
+import { fromEvent, Subscription, ReplaySubject, Subject, BehaviorSubject, Observable } from 'rxjs'
 import { pipeFromArray } from "rxjs/internal/util/pipe";
-import { debounceTime, filter, map, scan } from "rxjs/operators";
+import { debounceTime, filter, map, scan, startWith, debounce, tap } from "rxjs/operators";
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
 import { StateInterface as ViewerConfiguration } from "src/services/state/viewerConfig.store";
 import { getNgIdLabelIndexFromId } from "src/services/stateStore.service";
@@ -12,6 +12,7 @@ import { getExportNehuba, getViewer, setNehubaViewer } from "src/util/fn";
 
 import '!!file-loader?context=third_party&name=main.bundle.js!export-nehuba/dist/min/main.bundle.js'
 import '!!file-loader?context=third_party&name=chunk_worker.bundle.js!export-nehuba/dist/min/chunk_worker.bundle.js'
+import { scanSliceViewRenderFn } from "../util";
 
 const NG_LANDMARK_LAYER_NAME = 'spatial landmark layer'
 const NG_USER_LANDMARK_LAYER_NAME = 'user landmark layer'
@@ -63,6 +64,8 @@ const scanFn: (acc: LayerLabelIndex[], curr: LayerLabelIndex) => LayerLabelIndex
 
 export class NehubaViewerUnit implements OnInit, OnDestroy {
 
+  private sliceviewLoading$: Observable<boolean>
+
   public overrideShowLayers: string[] = []
   get showLayersName() {
     return [
@@ -413,10 +416,19 @@ export class NehubaViewerUnit implements OnInit, OnDestroy {
   }
 
   public ngOnInit() {
+    this.sliceviewLoading$ = fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent').pipe(
+      scan(scanSliceViewRenderFn, [ null, null, null ]),
+      map(arrOfFlags => arrOfFlags.some(flag => flag)),
+      startWith(true),
+    )
+
     this.subscriptions.push(
       this.loadMeshes$.pipe(
         scan(scanFn, []),
-        debounceTime(100)
+        debounceTime(100),
+        debounce(() => this.sliceviewLoading$.pipe(
+          filter(flag => !flag),
+        ))
       ).subscribe(layersLabelIndex => {
         let totalMeshes = 0
         for (const layerLayerIndex of layersLabelIndex) {
@@ -425,7 +437,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy {
           this.nehubaViewer.setMeshesToLoad(labelIndicies, layer)
         }
         // TODO implement total mesh to be loaded and mesh loading UI
-        // this.numMeshesToBeLoaded = totalMeshes
+        this.numMeshesToBeLoaded = totalMeshes
       }),
     )
 
diff --git a/src/ui/nehubaContainer/util.ts b/src/ui/nehubaContainer/util.ts
index f1fbb9348f338683c81fbabe86680140c2f3fc55..edadec62d60a7c10ff954a35b669aa0e90315412 100644
--- a/src/ui/nehubaContainer/util.ts
+++ b/src/ui/nehubaContainer/util.ts
@@ -214,3 +214,39 @@ export const importNehubaFactory = appendSrc => {
     return pr
   }
 }
+
+
+export const isFirstRow = (cell: HTMLElement) => {
+  const { parentElement: row } = cell
+  const { parentElement: container } = row
+  return container.firstElementChild === row
+}
+
+export const isFirstCell = (cell: HTMLElement) => {
+  const { parentElement: row } = cell
+  return row.firstElementChild === cell
+}
+
+export const scanSliceViewRenderFn: (acc: [boolean, boolean, boolean], curr: CustomEvent) => [boolean, boolean, boolean] = (acc, curr) => {
+  
+  const target = curr.target as HTMLElement
+  const targetIsFirstRow = isFirstRow(target)
+  const targetIsFirstCell = isFirstCell(target)
+  const idx = targetIsFirstRow
+    ? targetIsFirstCell
+      ? 0
+      : 1
+    : targetIsFirstCell
+      ? 2
+      : null
+
+  const returnAcc = [...acc]
+  const num1 = typeof curr.detail.missingChunks === 'number' ? curr.detail.missingChunks : 0
+  const num2 = typeof curr.detail.missingImageChunks === 'number' ? curr.detail.missingImageChunks : 0
+  if (num1 < 0 && num2 < 0) {
+    returnAcc[idx] = true
+  } else {
+    returnAcc[idx] = Math.max(num1, num2) > 0
+  }
+  return returnAcc as [boolean, boolean, boolean]
+}