diff --git a/docs/releases/v2.2.0.md b/docs/releases/v2.2.0.md
index c927a80ccc6ddf080cd1ebb26f9ca63e7015e0db..01cfb1dc21808d3cb19176ccbe834a8c65e176dd 100644
--- a/docs/releases/v2.2.0.md
+++ b/docs/releases/v2.2.0.md
@@ -9,3 +9,4 @@
 
 - Fixed false positive CSP violations (#490)
 - Fixed `standAloneVolumes` only showing the first volume
+- Fixed `pluginControl.loadExternalLibraries` and `pluginControl.unloadExternalLibraries` (#516)
diff --git a/src/atlasViewer/atlasViewer.apiService.service.spec.ts b/src/atlasViewer/atlasViewer.apiService.service.spec.ts
index 5260669fd60eb4e8d2bd69d9484c97239b0456b2..a2c77a18d54841ba51a0fbf8e752763317f972a5 100644
--- a/src/atlasViewer/atlasViewer.apiService.service.spec.ts
+++ b/src/atlasViewer/atlasViewer.apiService.service.spec.ts
@@ -5,7 +5,8 @@ import { defaultRootState } from "src/services/stateStore.service";
 import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module";
 import { HttpClientModule } from '@angular/common/http';
 import { WidgetModule } from './widgetUnit/widget.module';
-import { PluginModule } from './pluginUnit/plugin.module';
+import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
+import { PluginServices } from "./pluginUnit";
 
 describe('atlasViewer.apiService.service.ts', () => {
 
@@ -17,15 +18,17 @@ describe('atlasViewer.apiService.service.ts', () => {
     afterEach(() => {
       cancelTokenSpy.calls.reset()
       cancellableDialogSpy.calls.reset()
+
+      const ctrl = TestBed.inject(HttpTestingController)
+      ctrl.verify()
     })
 
     beforeEach(async(() => {
       TestBed.configureTestingModule({
         imports: [
           AngularMaterialModule,
-          HttpClientModule,
+          HttpClientTestingModule,
           WidgetModule,
-          PluginModule,
         ],
         providers: [
           AtlasViewerAPIServices,
@@ -33,6 +36,10 @@ describe('atlasViewer.apiService.service.ts', () => {
           {
             provide: CANCELLABLE_DIALOG,
             useValue: cancellableDialogSpy
+          },
+          {
+            provide: PluginServices,
+            useValue: {}
           }
         ]
       }).compileComponents()
@@ -250,7 +257,6 @@ describe('atlasViewer.apiService.service.ts', () => {
           AngularMaterialModule,
           HttpClientModule,
           WidgetModule,
-          PluginModule,
         ],
         providers: [
           {
@@ -267,6 +273,10 @@ describe('atlasViewer.apiService.service.ts', () => {
               return mockGetMouseOverSegments
             }
           },
+          {
+            provide: PluginServices,
+            useValue: {}
+          },
           AtlasViewerAPIServices,
           provideMockStore({ initialState: defaultRootState }),
         ]
diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts
index cf1f2d83e4b2fb03d8b42b8293ccf4af52392474..30cde5aded6877e7df1624f37333c1e17135d61b 100644
--- a/src/atlasViewer/atlasViewer.constantService.service.ts
+++ b/src/atlasViewer/atlasViewer.constantService.service.ts
@@ -338,13 +338,6 @@ Send us an email: <a target = "_blank" href = "mailto:${this.supportEmailAddress
   public dissmissUserLayerSnackbarMessage: string = this.dissmissUserLayerSnackbarMessageDesktop
 }
 
-const parseURLToElement = (url: string): HTMLElement => {
-  const el = document.createElement('script')
-  el.setAttribute('crossorigin', 'true')
-  el.src = url
-  return el
-}
-
 export const UNSUPPORTED_PREVIEW = [{
   text: 'Preview of Colin 27 and JuBrain Cytoarchitectonic',
   previewSrc: './res/image/1.png',
@@ -358,16 +351,6 @@ export const UNSUPPORTED_PREVIEW = [{
 
 export const UNSUPPORTED_INTERVAL = 7000
 
-export const SUPPORT_LIBRARY_MAP: Map<string, HTMLElement> = new Map([
-  ['jquery@3', parseURLToElement('https://code.jquery.com/jquery-3.3.1.min.js')],
-  ['jquery@2', parseURLToElement('https://code.jquery.com/jquery-2.2.4.min.js')],
-  ['webcomponentsLite@1.1.0', parseURLToElement('https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.1.0/webcomponents-lite.js')],
-  ['react@16', parseURLToElement('https://unpkg.com/react@16/umd/react.development.js')],
-  ['reactdom@16', parseURLToElement('https://unpkg.com/react-dom@16/umd/react-dom.development.js')],
-  ['vue@2.5.16', parseURLToElement('https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js')],
-  ['preact@8.4.2', parseURLToElement('https://cdn.jsdelivr.net/npm/preact@8.4.2/dist/preact.min.js')],
-  ['d3@5.7.0', parseURLToElement('https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js')],
-])
 
 /**
  * First attempt at encoding int (e.g. selected region, navigation location) from number (loc info density) to b64 (higher info density)
diff --git a/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts b/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts
index 22ed4cd0eeca8e72b4a66b0c67ee9fb1e387363d..aeecdfb1d35ccb00794f7e31c324e2c7362d279e 100644
--- a/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts
+++ b/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts
@@ -1,16 +1,25 @@
 import { HttpClient } from '@angular/common/http'
-import { ComponentFactory, ComponentFactoryResolver, Injectable, ViewContainerRef } from "@angular/core";
+import { ComponentFactory, ComponentFactoryResolver, Injectable, ViewContainerRef, Inject } from "@angular/core";
 import { PLUGINSTORE_ACTION_TYPES } from "src/services/state/pluginState.store";
 import { IavRootStoreInterface, isDefined } from 'src/services/stateStore.service'
 import { PluginUnit } from "./pluginUnit.component";
 import { WidgetServices } from "../widgetUnit/widgetService.service";
 import { select, Store } from "@ngrx/store";
-import { BehaviorSubject, merge, Observable, of } from "rxjs";
-import { filter, map, shareReplay } from "rxjs/operators";
+import { BehaviorSubject, merge, Observable, of, zip } from "rxjs";
+import { filter, map, shareReplay, switchMap, catchError } from "rxjs/operators";
 import { LoggingService } from 'src/logging';
 import { PluginHandler } from 'src/util/pluginHandler';
-import { AtlasViewerConstantsServices } from "../atlasViewer.constantService.service";
 import { WidgetUnit } from "../widgetUnit/widgetUnit.component";
+import { APPEND_SCRIPT_TOKEN, REMOVE_SCRIPT_TOKEN, BACKENDURL, getHttpHeader } from 'src/util/constants';
+import { PluginFactoryDirective } from './pluginFactory.directive';
+
+export const registerPluginFactoryDirectiveFactory = (pSer: PluginServices) => {
+  return (pFactoryDirective: PluginFactoryDirective) => {
+    pSer.loadExternalLibraries = pFactoryDirective.loadExternalLibraries.bind(pFactoryDirective)
+    pSer.unloadExternalLibraries = pFactoryDirective.unloadExternalLibraries.bind(pFactoryDirective)
+    pSer.pluginViewContainerRef = pFactoryDirective.viewContainerRef
+  }
+}
 
 @Injectable({
   providedIn : 'root',
@@ -25,8 +34,7 @@ export class PluginServices {
 
   public fetchedPluginManifests: IPluginManifest[] = []
   public pluginViewContainerRef: ViewContainerRef
-  public appendSrc: (script: HTMLElement) => void
-  public removeSrc: (script: HTMLElement) => void
+
   private pluginUnitFactory: ComponentFactory<PluginUnit>
   public minimisedPlugins$: Observable<Set<string>>
 
@@ -36,12 +44,13 @@ export class PluginServices {
   public fetch: (url: string, httpOption?: any) => Promise<any> = (url, httpOption = {}) => this.http.get(url, httpOption).toPromise()
 
   constructor(
-    private constantService: AtlasViewerConstantsServices,
     private widgetService: WidgetServices,
     private cfr: ComponentFactoryResolver,
     private store: Store<IavRootStoreInterface>,
     private http: HttpClient,
     private log: LoggingService,
+    @Inject(APPEND_SCRIPT_TOKEN) private appendSrc: (src: string) => Promise<HTMLScriptElement>,
+    @Inject(REMOVE_SCRIPT_TOKEN) private removeSrc: (src: HTMLScriptElement) => void,
   ) {
 
     // TODO implement
@@ -56,46 +65,36 @@ export class PluginServices {
     /**
      * TODO convert to rxjs streams, instead of Promise.all
      */
-    const promiseFetchedPluginManifests: Promise<IPluginManifest[]> = new Promise((resolve, reject) => {
-      Promise.all([
-        // TODO convert to use this.fetch
-        PLUGINDEV
-          ? fetch(PLUGINDEV, this.constantService.getFetchOption()).then(res => res.json())
-          : Promise.resolve([]),
-        new Promise(rs => {
-          fetch(`${this.constantService.backendUrl}plugins`, this.constantService.getFetchOption())
-            .then(res => res.json())
-            .then(arr => Promise.all(
-              arr.map(url => new Promise(rs2 =>
-                /**
-                 * instead of failing all promises when fetching manifests, only fail those that fails to fetch
-                 */
-                fetch(url, this.constantService.getFetchOption()).then(res => res.json()).then(rs2).catch(e => (this.log.log('fetching manifest error', e), rs2(null)))),
-              ),
-            ))
-            .then(manifests => rs(
-              manifests.filter(m => !!m),
-            ))
-            .catch(e => {
-              this.constantService.catchError(e)
-              rs([])
-            })
-        }),
-        Promise.all(
-          BUNDLEDPLUGINS
-            .filter(v => typeof v === 'string')
-            .map(v => fetch(`res/plugin_examples/${v}/manifest.json`, this.constantService.getFetchOption()).then(res => res.json())),
+
+    const pluginUrl = `${BACKENDURL.replace(/\/$/,'')}/plugins`
+    const streamFetchedManifests$ = this.http.get(pluginUrl,{
+      responseType: 'json',
+      headers: getHttpHeader(),
+    }).pipe(
+      switchMap((arr: string[]) => {
+        return zip(
+          ...arr.map(url => this.http.get(url, {
+            responseType: 'json',
+            headers: getHttpHeader()
+          }).pipe(
+            catchError((err, caught) => of(null))
+          ))
+        ).pipe(
+          map(arr => arr.filter(v => !!v))
         )
-          .then(arr => arr.reduce((acc, curr) => acc.concat(curr) , [])),
-      ])
-        .then(arr => resolve( [].concat(arr[0]).concat(arr[1]) ))
-        .catch(reject)
-    })
+      })
+    )
 
-    promiseFetchedPluginManifests
-      .then(arr =>
-        this.fetchedPluginManifests = arr)
-      .catch(this.log.error)
+    streamFetchedManifests$.subscribe(
+      arr => {
+        this.fetchedPluginManifests = arr
+        this.log.log(this.fetchedPluginManifests)
+      },
+      this.log.error,
+      () => {
+        this.log.log(`fetching end`)
+      }
+    )
 
     this.minimisedPlugins$ = merge(
       of(new Set()),
@@ -191,7 +190,7 @@ export class PluginServices {
     this.addPluginToIsLaunchingSet(plugin.name)
 
     return this.readyPlugin(plugin)
-      .then(() => {
+      .then(async () => {
         const pluginUnit = this.pluginViewContainerRef.createComponent( this.pluginUnitFactory )
         /* TODO in v0.2, I used:
 
@@ -240,11 +239,8 @@ export class PluginServices {
           shutdownCB.push(cb)
         }
 
-        const script = document.createElement('script')
-        script.src = plugin.scriptURL
-
-        this.appendSrc(script)
-        handler.onShutdown(() => this.removeSrc(script))
+        const scriptEl = await this.appendSrc(plugin.scriptURL)
+        handler.onShutdown(() => this.removeSrc(scriptEl))
 
         const template = document.createElement('div')
         template.insertAdjacentHTML('afterbegin', plugin.template)
diff --git a/src/atlasViewer/pluginUnit/plugin.module.ts b/src/atlasViewer/pluginUnit/plugin.module.ts
index 88198bfe685c2613faee04332ae417ffd6e9ee1c..12763521faa0539bdf7eb38d7b275eba8263dcb7 100644
--- a/src/atlasViewer/pluginUnit/plugin.module.ts
+++ b/src/atlasViewer/pluginUnit/plugin.module.ts
@@ -1,8 +1,10 @@
 import { NgModule } from "@angular/core";
 import { PluginUnit } from "./pluginUnit.component";
-import { PluginServices } from "./atlasViewer.pluginService.service";
-import { PluginFactoryDirective } from "./pluginFactory.directive";
+import { PluginServices, registerPluginFactoryDirectiveFactory } from "./atlasViewer.pluginService.service";
+import { PluginFactoryDirective, REGISTER_PLUGIN_FACTORY_DIRECTIVE } from "./pluginFactory.directive";
 import { LoggingModule } from "src/logging";
+import { APPEND_SCRIPT_TOKEN, appendScriptFactory, REMOVE_SCRIPT_TOKEN, removeScriptFactory } from "src/util/constants";
+import { DOCUMENT } from "@angular/common";
 
 @NgModule({
   imports:[
@@ -20,7 +22,22 @@ import { LoggingModule } from "src/logging";
     PluginFactoryDirective
   ],
   providers: [
-    PluginServices
+    PluginServices,
+    {
+      provide: REGISTER_PLUGIN_FACTORY_DIRECTIVE,
+      useFactory: registerPluginFactoryDirectiveFactory,
+      deps: [ PluginServices ]
+    },
+    {
+      provide: APPEND_SCRIPT_TOKEN,
+      useFactory: appendScriptFactory,
+      deps: [ DOCUMENT ]
+    },
+    {
+      provide: REMOVE_SCRIPT_TOKEN,
+      useFactory: removeScriptFactory,
+      deps: [ DOCUMENT ]
+    },
   ]
 })
 
diff --git a/src/atlasViewer/pluginUnit/pluginFactory.directive.spec.ts b/src/atlasViewer/pluginUnit/pluginFactory.directive.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29d723d972543473d647354c82dd829dfaa4dda3
--- /dev/null
+++ b/src/atlasViewer/pluginUnit/pluginFactory.directive.spec.ts
@@ -0,0 +1,133 @@
+import { async, TestBed } from "@angular/core/testing"
+import { PluginFactoryDirective, REGISTER_PLUGIN_FACTORY_DIRECTIVE } from "./pluginFactory.directive"
+import { Component, ViewChild } from "@angular/core"
+import { APPEND_SCRIPT_TOKEN, REMOVE_SCRIPT_TOKEN } from "src/util/constants"
+import { By } from "@angular/platform-browser"
+
+@Component({
+  template: '<div></div>'
+})
+class TestCmp{
+
+  @ViewChild(PluginFactoryDirective) pfd: PluginFactoryDirective
+}
+
+const dummyObj1 = {}
+const dummyObj2 = {}
+const appendSrcSpy = jasmine.createSpy('appendSrc').and.returnValues(
+  Promise.resolve(dummyObj1),
+  Promise.resolve(dummyObj2)
+)
+const removeSrcSpy = jasmine.createSpy('removeScript')
+const registerSpy = jasmine.createSpy('registerSpy')
+
+describe(`> pluginFactory.directive.ts`, () => {
+  describe(`> PluginFactoryDirective`, () => {
+
+    beforeEach(async(() => {
+      TestBed.configureTestingModule({
+        declarations: [
+          PluginFactoryDirective,
+          TestCmp
+        ],
+        providers: [
+          {
+            provide: APPEND_SCRIPT_TOKEN,
+            useValue: appendSrcSpy
+          },
+          {
+            provide: REMOVE_SCRIPT_TOKEN,
+            useValue: removeSrcSpy
+          },
+          {
+            provide: REGISTER_PLUGIN_FACTORY_DIRECTIVE,
+            useValue: registerSpy
+          }
+        ]
+      }).overrideComponent(TestCmp, {
+        set: {
+          template: `<div pluginFactoryDirective></div>`
+        }
+      }).compileComponents()
+    }))
+
+    afterEach(() => {
+      appendSrcSpy.calls.reset()
+      removeSrcSpy.calls.reset()
+      registerSpy.calls.reset()
+    })
+
+    it('> creates directive', () => {
+      const fixture = TestBed.createComponent(TestCmp)
+      fixture.detectChanges()
+
+      const queriedDirective = fixture.debugElement.query( By.directive(PluginFactoryDirective) )
+      expect(queriedDirective).toBeTruthy()
+    })
+
+    it('> register spy is called', () => {
+      
+      const fixture = TestBed.createComponent(TestCmp)
+      fixture.detectChanges()
+      expect(registerSpy).toHaveBeenCalledWith(fixture.componentInstance.pfd)
+    })
+
+    describe('> loading external libraries', () => {
+      it('> load once, call append script', async () => {
+        const fixture = TestBed.createComponent(TestCmp)
+        fixture.detectChanges()
+        const pfd = fixture.componentInstance.pfd
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        expect(appendSrcSpy).toHaveBeenCalledWith('https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js')
+        expect(appendSrcSpy).toHaveBeenCalledTimes(1)
+      })
+
+      it('> load twice, called append script once', async () => {
+        const fixture = TestBed.createComponent(TestCmp)
+        fixture.detectChanges()
+        const pfd = fixture.componentInstance.pfd
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        expect(appendSrcSpy).toHaveBeenCalledWith('https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js')
+        expect(appendSrcSpy).toHaveBeenCalledTimes(1)
+      })
+
+      it('> load unload, call remove script once', async () => {
+        
+        const fixture = TestBed.createComponent(TestCmp)
+        fixture.detectChanges()
+        const pfd = fixture.componentInstance.pfd
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        pfd.unloadExternalLibraries(['vue@2.5.16'])
+        expect(removeSrcSpy).toHaveBeenCalledTimes(1)
+      })
+
+      it('> load twice, unload, does not call remove', async () => {
+
+        const fixture = TestBed.createComponent(TestCmp)
+        fixture.detectChanges()
+        const pfd = fixture.componentInstance.pfd
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        pfd.unloadExternalLibraries(['vue@2.5.16'])
+        expect(removeSrcSpy).not.toHaveBeenCalled()
+      })
+
+      it('> load, unload, load, call append script twice', async () => {
+        
+        const fixture = TestBed.createComponent(TestCmp)
+        fixture.detectChanges()
+        const pfd = fixture.componentInstance.pfd
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        pfd.unloadExternalLibraries(['vue@2.5.16'])
+
+        appendSrcSpy.calls.reset()
+        expect(appendSrcSpy).not.toHaveBeenCalled()
+
+        await pfd.loadExternalLibraries(['vue@2.5.16'])
+        pfd.unloadExternalLibraries(['vue@2.5.16'])
+        expect(appendSrcSpy).toHaveBeenCalledTimes(1)
+      })
+    })
+  })
+})
\ No newline at end of file
diff --git a/src/atlasViewer/pluginUnit/pluginFactory.directive.ts b/src/atlasViewer/pluginUnit/pluginFactory.directive.ts
index 65081f45eb78a0506b9997b171975f7026c31cb1..fbc76ac7bf830258620539a0301a4cac71778713 100644
--- a/src/atlasViewer/pluginUnit/pluginFactory.directive.ts
+++ b/src/atlasViewer/pluginUnit/pluginFactory.directive.ts
@@ -1,7 +1,44 @@
-import { Directive, Renderer2, ViewContainerRef } from "@angular/core";
-import { SUPPORT_LIBRARY_MAP } from "src/atlasViewer/atlasViewer.constantService.service";
-import { PluginServices } from "./atlasViewer.pluginService.service";
-import { LoggingService } from "src/logging";
+import { Directive, ViewContainerRef, Inject, Optional } from "@angular/core";
+import { APPEND_SCRIPT_TOKEN, REMOVE_SCRIPT_TOKEN } from "src/util/constants";
+
+export const SUPPORT_LIBRARY_MAP: Map<string, Map<string, string>> = new Map([
+  ['jquery', new Map<string, string>([
+    ['3', 'https://code.jquery.com/jquery-3.3.1.min.js'],
+    ['2', 'https://code.jquery.com/jquery-2.2.4.min.js']
+  ])],
+  ['webcomponentsLite', new Map([
+    ['1.1.0', 'https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.1.0/webcomponents-lite.js']
+  ])],
+  ['react', new Map([
+    ['16', 'https://unpkg.com/react@16/umd/react.development.js']
+  ])],
+  ['reactdom', new Map([
+    ['16', 'https://unpkg.com/react-dom@16/umd/react-dom.development.js']
+  ])],
+  ['vue', new Map([
+    ['2.5.16', 'https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js']
+  ])],
+  ['preact', new Map([
+    ['8.4.2', 'https://cdn.jsdelivr.net/npm/preact@8.4.2/dist/preact.min.js']
+  ])],
+  ['d3', new Map([
+    ['5.7.0', 'https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js']
+  ])],
+])
+
+export const parseLibrary = (libVer: string) => {
+  const re = /^([a-zA-Z0-9]+)@([0-9.]+)$/.exec(libVer)
+  if (!re) throw new Error(`${libVer} cannot be parsed properly`)
+  const lib = re[1]
+  const ver = re[2]
+  const libMap = SUPPORT_LIBRARY_MAP.get(lib) 
+  if (!libMap) throw new Error(`${lib} not supported. Only supported libraries are ${Array.from(SUPPORT_LIBRARY_MAP.keys())}`)
+  const src = libMap.get(ver)
+  if (!src) throw new Error(`${lib} version ${ver} not supported. Only supports ${Array.from(libMap.keys())}`)
+  return src
+}
+
+export const REGISTER_PLUGIN_FACTORY_DIRECTIVE = `REGISTER_PLUGIN_FACTORY_DIRECTIVE`
 
 @Directive({
   selector: '[pluginFactoryDirective]',
@@ -9,72 +46,53 @@ import { LoggingService } from "src/logging";
 
 export class PluginFactoryDirective {
   constructor(
-    pluginService: PluginServices,
-    viewContainerRef: ViewContainerRef,
-    private rd2: Renderer2,
-    private log: LoggingService,
+    public viewContainerRef: ViewContainerRef,
+    @Optional() @Inject(REGISTER_PLUGIN_FACTORY_DIRECTIVE) registerPluginFactoryDirective: (directive: PluginFactoryDirective) => void,
+    @Inject(APPEND_SCRIPT_TOKEN) private appendScript: (src: string) => Promise<HTMLScriptElement>,
+    @Inject(REMOVE_SCRIPT_TOKEN) private removeScript: (srcEl: HTMLScriptElement) => void,
   ) {
-    pluginService.loadExternalLibraries = this.loadExternalLibraries.bind(this)
-    pluginService.unloadExternalLibraries = this.unloadExternalLibraries.bind(this)
-    pluginService.pluginViewContainerRef = viewContainerRef
-    pluginService.appendSrc = (src: HTMLElement) => rd2.appendChild(document.head, src)
-    pluginService.removeSrc = (src: HTMLElement) => rd2.removeChild(document.head, src)
+    if (registerPluginFactoryDirective) {
+      registerPluginFactoryDirective(this)
+    }
   }
 
-  private loadedLibraries: Map<string, {counter: number, src: HTMLElement|null}> = new Map()
+  private loadedLibraries: Map<string, {counter: number, srcEl: HTMLScriptElement|null}> = new Map()
   
-  loadExternalLibraries(libraries: string[]) {
-    const srcHTMLElement = libraries.map(libraryName => ({
-      name: libraryName,
-      srcEl: SUPPORT_LIBRARY_MAP.get(libraryName),
-    }))
+  async loadExternalLibraries(libraries: string[]) {
+    const libsToBeLoaded = libraries.map(libName => {
+      return {
+        libName,
+        libSrc: parseLibrary(libName),
+      }
+    })
 
-    const rejected = srcHTMLElement.filter(scriptObj => scriptObj.srcEl === null)
-    if (rejected.length > 0) {
-      return Promise.reject(`Some library names cannot be recognised. No libraries were loaded: ${rejected.map(srcObj => srcObj.name).join(', ')}`)
-    }
+    for (const libToBeLoaded of libsToBeLoaded) {
+  
+      const { libSrc, libName } = libToBeLoaded
 
-    return Promise.all(srcHTMLElement.map(scriptObj => new Promise((rs, rj) => {
-      /**
-       * if browser already support customElements, do not append polyfill
-       */
-      if ('customElements' in window && scriptObj.name === 'webcomponentsLite') {
-        return rs()
-      }
-      const existingEntry = this.loadedLibraries.get(scriptObj.name)
-      if (existingEntry) {
-        this.loadedLibraries.set(scriptObj.name, { counter: existingEntry.counter + 1, src: existingEntry.src })
-        rs()
-      } else {
-        const srcEl = scriptObj.srcEl
-        srcEl.onload = () => rs()
-        srcEl.onerror = (e: any) => rj(e)
-        this.rd2.appendChild(document.head, srcEl)
-        this.loadedLibraries.set(scriptObj.name, { counter: 1, src: srcEl })
+      // if browser natively support custom element, do not append polyfill
+      if ('customElements' in window && /^webcomponentsLite@/.test(libName)) continue
+
+      let srcEl
+      const { counter, srcEl: srcElOld } = this.loadedLibraries.get(libName) || { counter: 0 }
+      if (counter === 0) {
+
+        // slight performance penalty not loading external libraries in parallel, but this should be an edge case any way
+        srcEl = await this.appendScript(libSrc)
       }
-    })))
+      this.loadedLibraries.set(libName, { counter: counter + 1, srcEl: srcEl || srcElOld })
+    }
   }
 
   unloadExternalLibraries(libraries: string[]) {
-    libraries
-      .filter((stringname) => SUPPORT_LIBRARY_MAP.get(stringname) !== null)
-      .forEach(libname => {
-        const ledger = this.loadedLibraries.get(libname)
-        if (!ledger) {
-          this.log.warn('unload external libraries error. cannot find ledger entry...', libname, this.loadedLibraries)
-          return
-        }
-        if (ledger.src === null) {
-          this.log.log('webcomponents is native supported. no library needs to be unloaded')
-          return
-        }
-
-        if (ledger.counter - 1 == 0) {
-          this.rd2.removeChild(document.head, ledger.src)
-          this.loadedLibraries.delete(libname)
-        } else {
-          this.loadedLibraries.set(libname, { counter: ledger.counter - 1, src: ledger.src })
-        }
-      })
+    for (const lib of libraries) {
+      const { counter, srcEl } = this.loadedLibraries.get(lib) || { counter: 0 }
+      if (counter > 1) {
+        this.loadedLibraries.set(lib, { counter: counter - 1, srcEl })
+      } else {
+        this.loadedLibraries.set(lib, { counter: 0, srcEl: null })
+        this.removeScript(srcEl)
+      }
+    }
   }
 }
diff --git a/src/main.module.ts b/src/main.module.ts
index 86d315965b857704c0bde1f27eb391d8090b1b18..b7716994d227ae593909b5c19ee19763bbbf3745 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -1,8 +1,8 @@
 import { DragDropModule } from '@angular/cdk/drag-drop'
-import { CommonModule } from "@angular/common";
+import { CommonModule, DOCUMENT } from "@angular/common";
 import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core";
 import { FormsModule } from "@angular/forms";
-import { StoreModule, Store, select } from "@ngrx/store";
+import { StoreModule, Store } from "@ngrx/store";
 import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module'
 import { AtlasViewer, NEHUBA_CLICK_OVERRIDE } from "./atlasViewer/atlasViewer.component";
 import { ComponentsModule } from "./components/components.module";
@@ -52,6 +52,7 @@ import 'hammerjs'
 import 'src/res/css/extra_styles.css'
 import 'src/res/css/version.css'
 import 'src/theme.scss'
+import { APPEND_SCRIPT_TOKEN, appendScriptFactory, REMOVE_SCRIPT_TOKEN, removeScriptFactory } from './util/constants';
 
 @NgModule({
   imports : [
@@ -180,6 +181,7 @@ import 'src/theme.scss'
       deps: [ UIService ]
     },
 
+
     /**
      * TODO
      * once nehubacontainer is separated into viewer + overlay, migrate to nehubaContainer module
diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.spec.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.spec.ts
index 76e62d2f97229bb56498b68ebc5f72c17beaf8ea..a85e3c733286b7fdbe2c7517fb164a9c1c93bdbc 100644
--- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.spec.ts
+++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.spec.ts
@@ -4,6 +4,7 @@ import { NehubaViewerUnit, IMPORT_NEHUBA_INJECT_TOKEN } from "./nehubaViewer.com
 import { importNehubaFactory } from "../util"
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"
 import { LoggingModule } from "src/logging"
+import { APPEND_SCRIPT_TOKEN, appendScriptFactory } from "src/util/constants"
 
 
 describe('nehubaViewer.component,ts', () => {
@@ -21,6 +22,11 @@ describe('nehubaViewer.component,ts', () => {
           {
             provide: IMPORT_NEHUBA_INJECT_TOKEN,
             useFactory: importNehubaFactory,
+            deps: [ APPEND_SCRIPT_TOKEN ]
+          },
+          {
+            provide: APPEND_SCRIPT_TOKEN,
+            useFactory: appendScriptFactory,
             deps: [ DOCUMENT ]
           },
           AtlasWorkerService
diff --git a/src/ui/nehubaContainer/util.ts b/src/ui/nehubaContainer/util.ts
index 1a622c8eaeb4f393516908ba1ba2be559962568f..f1fbb9348f338683c81fbabe86680140c2f3fc55 100644
--- a/src/ui/nehubaContainer/util.ts
+++ b/src/ui/nehubaContainer/util.ts
@@ -203,19 +203,13 @@ export const userLmUnchanged = (oldlms, newlms) => {
     && newlms.every(lm => singleLmUnchanged(lm, oldmap as Map<string, [number, number, number]>))
 }
 
-export const importNehubaFactory = (document: Document) => {
+export const importNehubaFactory = appendSrc => {
   let pr: Promise<any>
   return () => {
     if ((window as any).export_nehuba) return Promise.resolve()
 
     if (pr) return pr
-    pr = new Promise((rs, rj) => {
-      const scriptEl = document.createElement('script')
-      scriptEl.src = 'main.bundle.js'
-      scriptEl.onload = () => rs()
-      scriptEl.onerror = (e) => rj(e)
-      document.head.appendChild(scriptEl)
-    })
+    pr = appendSrc('main.bundle.js')
 
     return pr
   }
diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts
index a66be01bb9d707d5d52051ee7dd24e29e81eeb5e..b58d1b66231b6966622ac15e7fd9f6227a310053 100644
--- a/src/ui/ui.module.ts
+++ b/src/ui/ui.module.ts
@@ -87,8 +87,9 @@ import { AuthModule } from "src/auth";
 import { FabSpeedDialModule } from "src/components/fabSpeedDial";
 import { ActionDialog } from "./actionDialog/actionDialog.component";
 import { NehubaViewerTouchDirective } from "./nehubaContainer/nehubaViewerInterface/nehubaViewerTouch.directive";
-import { DOCUMENT } from "@angular/common";
 import { importNehubaFactory } from "./nehubaContainer/util";
+import { APPEND_SCRIPT_TOKEN, appendScriptFactory } from "src/util/constants";
+import { DOCUMENT } from "@angular/common";
 
 
 @NgModule({
@@ -189,6 +190,11 @@ import { importNehubaFactory } from "./nehubaContainer/util";
     {
       provide: IMPORT_NEHUBA_INJECT_TOKEN,
       useFactory: importNehubaFactory,
+      deps: [ APPEND_SCRIPT_TOKEN ]
+    },
+    {
+      provide: APPEND_SCRIPT_TOKEN,
+      useFactory: appendScriptFactory,
       deps: [ DOCUMENT ]
     }
   ],
diff --git a/src/util/constants.ts b/src/util/constants.ts
index af922497ebe53f2d7c309a58a8fc4d61fbdace1e..901f704818fc711f396f6dbce6a5b525e08e1a05 100644
--- a/src/util/constants.ts
+++ b/src/util/constants.ts
@@ -1,3 +1,5 @@
+import { HttpHeaders } from "@angular/common/http"
+
 export const LOCAL_STORAGE_CONST = {
   GPU_LIMIT: 'fzj.xg.iv.GPU_LIMIT',
   ANIMATION: 'fzj.xg.iv.ANIMATION_FLAG',
@@ -18,4 +20,43 @@ export const MIN_REQ_EXPLAINER = `
 - Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float\` extension enabled.
 - You can check browsers' support of webgl2.0 by visiting <https://caniuse.com/#feat=webgl2>
 - Unfortunately, Safari and iOS devices currently do not support **webgl2.0**: <https://webkit.org/status/#specification-webgl-2>
-`
\ No newline at end of file
+`
+
+export const APPEND_SCRIPT_TOKEN = `APPEND_SCRIPT_TOKEN`
+
+export const appendScriptFactory = (document: Document) => {
+  return src => new Promise((rs, rj) => {
+    const scriptEl = document.createElement('script')
+    scriptEl.src = src
+    scriptEl.onload = () => rs(scriptEl)
+    scriptEl.onerror = (e) => rj(e)
+    document.head.appendChild(scriptEl)
+  })
+}
+
+export const REMOVE_SCRIPT_TOKEN = `REMOVE_SCRIPT_TOKEN`
+
+export const removeScriptFactory = (document: Document) => {
+  return (srcEl: HTMLScriptElement) => {
+    document.head.removeChild(srcEl)
+  }
+}
+
+const getScopedReferer = () => {
+  const url = new URL(window.location.href)
+  url.searchParams.delete('regionsSelected')
+  url.searchParams.delete('cRegionsSelected')
+  return url.toString()
+}
+
+export const getFetchOption: () => Partial<RequestInit> = () => {
+  return {
+    referrer: getScopedReferer()
+  }
+}
+
+export const getHttpHeader: () => HttpHeaders = () => {
+  const header = new HttpHeaders()
+  header.set('referrer', getScopedReferer())
+  return header
+}
\ No newline at end of file