diff --git a/examples/index.html b/examples/index.html
index dfa8abb5..7fa9d7dd 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -36,7 +36,7 @@
     
 
     
   
 
diff --git a/package-lock.json b/package-lock.json
index aff95591..c64ceda2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3804,6 +3804,32 @@
         "fastq": "^1.6.0"
       }
     },
+    "@rollup/plugin-node-resolve": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-10.0.0.tgz",
+      "integrity": "sha512-sNijGta8fqzwA1VwUEtTvWCx2E7qC70NMsDh4ZG13byAXYigBNZMxALhKUSycBks5gupJdq0lFrKumFrRZ8H3A==",
+      "dev": true,
+      "requires": {
+        "@rollup/pluginutils": "^3.1.0",
+        "@types/resolve": "1.17.1",
+        "builtin-modules": "^3.1.0",
+        "deepmerge": "^4.2.2",
+        "is-module": "^1.0.0",
+        "resolve": "^1.17.0"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.19.0",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
+          "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
+          "dev": true,
+          "requires": {
+            "is-core-module": "^2.1.0",
+            "path-parse": "^1.0.6"
+          }
+        }
+      }
+    },
     "@rollup/pluginutils": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
@@ -3963,6 +3989,12 @@
       "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
       "dev": true
     },
+    "@types/lodash": {
+      "version": "4.14.165",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz",
+      "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==",
+      "dev": true
+    },
     "@types/minimatch": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -3987,6 +4019,15 @@
       "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==",
       "dev": true
     },
+    "@types/resolve": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
+      "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/selenium-webdriver": {
       "version": "4.0.10",
       "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.0.10.tgz",
@@ -4736,6 +4777,12 @@
       "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
       "dev": true
     },
+    "builtin-modules": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
+      "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
+      "dev": true
+    },
     "cache-base": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@@ -6500,6 +6547,15 @@
         "ci-info": "^2.0.0"
       }
     },
+    "is-core-module": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz",
+      "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.3"
+      }
+    },
     "is-data-descriptor": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
@@ -6585,6 +6641,12 @@
         "is-extglob": "^2.1.1"
       }
     },
+    "is-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+      "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
+      "dev": true
+    },
     "is-negative-zero": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
@@ -7012,17 +7074,45 @@
       }
     },
     "jest-diff": {
-      "version": "26.6.1",
-      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.1.tgz",
-      "integrity": "sha512-BBNy/zin2m4kG5In126O8chOBxLLS/XMTuuM2+YhgyHk87ewPzKTuTJcqj3lOWOi03NNgrl+DkMeV/exdvG9gg==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz",
+      "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==",
       "dev": true,
       "requires": {
         "chalk": "^4.0.0",
-        "diff-sequences": "^26.5.0",
+        "diff-sequences": "^26.6.2",
         "jest-get-type": "^26.3.0",
-        "pretty-format": "^26.6.1"
+        "pretty-format": "^26.6.2"
       },
       "dependencies": {
+        "@jest/types": {
+          "version": "26.6.2",
+          "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
+          "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==",
+          "dev": true,
+          "requires": {
+            "@types/istanbul-lib-coverage": "^2.0.0",
+            "@types/istanbul-reports": "^3.0.0",
+            "@types/node": "*",
+            "@types/yargs": "^15.0.0",
+            "chalk": "^4.0.0"
+          }
+        },
+        "@types/istanbul-reports": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz",
+          "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==",
+          "dev": true,
+          "requires": {
+            "@types/istanbul-lib-report": "*"
+          }
+        },
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
         "chalk": {
           "version": "4.1.0",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
@@ -7034,9 +7124,9 @@
           }
         },
         "diff-sequences": {
-          "version": "26.5.0",
-          "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.5.0.tgz",
-          "integrity": "sha512-ZXx86srb/iYy6jG71k++wBN9P9J05UNQ5hQHQd9MtMPvcqXPx/vKU69jfHV637D00Q2gSgPk2D+jSx3l1lDW/Q==",
+          "version": "26.6.2",
+          "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
+          "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==",
           "dev": true
         },
         "jest-get-type": {
@@ -7044,6 +7134,24 @@
           "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz",
           "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==",
           "dev": true
+        },
+        "pretty-format": {
+          "version": "26.6.2",
+          "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
+          "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
+          "dev": true,
+          "requires": {
+            "@jest/types": "^26.6.2",
+            "ansi-regex": "^5.0.0",
+            "ansi-styles": "^4.0.0",
+            "react-is": "^17.0.1"
+          }
+        },
+        "react-is": {
+          "version": "17.0.1",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",
+          "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==",
+          "dev": true
         }
       }
     },
@@ -7996,10 +8104,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.19",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
-      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
-      "dev": true
+      "version": "4.17.20",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
     },
     "lodash.memoize": {
       "version": "4.1.2",
diff --git a/package.json b/package.json
index 2789571d..d0da1e56 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,10 @@
   "devDependencies": {
     "@babel/preset-env": "^7.9.5",
     "@babel/runtime-corejs3": "^7.9.2",
+    "@rollup/plugin-node-resolve": "^10.0.0",
     "@types/googlemaps": "^3.39.3",
     "@types/jest": "^26.0.10",
+    "@types/lodash": "^4.14.165",
     "@types/selenium-webdriver": "^4.0.9",
     "@typescript-eslint/eslint-plugin": ">=2.25.0",
     "@typescript-eslint/parser": ">=2.25.0",
@@ -56,5 +58,8 @@
   "publishConfig": {
     "access": "public",
     "registry": "https://wombat-dressing-room.appspot.com"
+  },
+  "dependencies": {
+    "lodash": "^4.17.20"
   }
 }
diff --git a/rollup.config.js b/rollup.config.js
index 5756fbd7..a4fb9cb1 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -16,6 +16,7 @@
 
 import babel from "rollup-plugin-babel";
 import commonjs from "rollup-plugin-commonjs";
+import { nodeResolve } from "@rollup/plugin-node-resolve";
 import { terser } from "rollup-plugin-terser";
 import typescript from "rollup-plugin-typescript2";
 
@@ -25,11 +26,16 @@ const babelOptions = {
 
 const terserOptions = { output: { comments: "" } };
 
+const resolveOptions = {
+  mainFields: ["browser", "jsnext:main", "module", "main"],
+};
+
 export default [
   {
     input: "src/index.ts",
     plugins: [
       typescript(),
+      nodeResolve(resolveOptions),
       commonjs(),
       babel(babelOptions),
       terser(terserOptions),
@@ -45,6 +51,7 @@ export default [
     input: "src/index.ts",
     plugins: [
       typescript(),
+      nodeResolve(resolveOptions),
       commonjs(),
       babel(babelOptions),
       terser(terserOptions),
@@ -57,7 +64,12 @@ export default [
   },
   {
     input: "src/index.ts",
-    plugins: [typescript(), commonjs(), babel(babelOptions)],
+    plugins: [
+      typescript(),
+      nodeResolve(resolveOptions),
+      commonjs(),
+      babel(babelOptions),
+    ],
     output: {
       file: "dist/index.dev.js",
       format: "iife",
@@ -66,7 +78,7 @@ export default [
   },
   {
     input: "src/index.ts",
-    plugins: [typescript()],
+    plugins: [typescript(), nodeResolve(resolveOptions), commonjs()],
     output: {
       file: "dist/index.esm.js",
       format: "esm",
diff --git a/src/index.test.ts b/src/index.test.ts
index a63faff3..bb9c7b98 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -18,6 +18,7 @@ import { Loader, LoaderOptions } from ".";
 
 afterEach(() => {
   document.getElementsByTagName("html")[0].innerHTML = "";
+  delete Loader["instance"];
 });
 
 test.each([
@@ -108,20 +109,50 @@ test("loadCallback callback should fire", () => {
   window.__googleMapsCallback(null);
 });
 
-test("script onerror should reject promise", () => {
+test("script onerror should reject promise", async () => {
   const loader = new Loader({ apiKey: "foo" });
 
-  expect.assertions(3);
+  const rejection = expect(loader.load()).rejects.toBeInstanceOf(ErrorEvent);
 
-  const promise = loader.load().catch((e) => {
-    expect(e).toBeTruthy();
-    expect(loader["done"]).toBeTruthy();
-    expect(loader["loading"]).toBeFalsy();
-  });
+  loader["loadErrorCallback"](document.createEvent("ErrorEvent"));
+
+  await rejection;
+  expect(loader["done"]).toBeTruthy();
+  expect(loader["loading"]).toBeFalsy();
+});
+
+test("script onerror should reject promise with multiple loaders", async () => {
+  const loader = new Loader({ apiKey: "foo" });
+  const extraLoader = new Loader({ apiKey: "foo" });
 
+  let rejection = expect(loader.load()).rejects.toBeInstanceOf(ErrorEvent);
   loader["loadErrorCallback"](document.createEvent("ErrorEvent"));
 
-  return promise;
+  await rejection;
+  expect(loader["done"]).toBeTruthy();
+  expect(loader["loading"]).toBeFalsy();
+  expect(loader["onerrorEvent"]).toBeInstanceOf(ErrorEvent);
+  rejection = expect(extraLoader.load()).rejects.toBeInstanceOf(ErrorEvent);
+
+  await rejection;
+  expect(extraLoader["done"]).toBeTruthy();
+  expect(extraLoader["loading"]).toBeFalsy();
+});
+
+test("singleton should be used", () => {
+  const loader = new Loader({ apiKey: "foo" });
+  const extraLoader = new Loader({ apiKey: "foo" });
+  expect(extraLoader).toBe(loader);
+
+  loader["done"] = true;
+  expect(extraLoader["done"]).toBe(loader["done"]);
+});
+
+test("singleton should throw with different options", () => {
+  new Loader({ apiKey: "foo" });
+  expect(() => {
+    new Loader({ apiKey: "bar" });
+  }).toThrowError();
 });
 
 test("loader should resolve immediately when successfully loaded", async () => {
diff --git a/src/index.ts b/src/index.ts
index 7407b5bf..c01fe598 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import isEqual from "lodash/fp/isEqual";
+
 /**
  * @ignore
  */
@@ -42,7 +44,7 @@ export interface LoaderOptions {
    */
   apiKey: string;
   /**
-  * @deprecated See https://developers.google.com/maps/premium/overview.
+   * @deprecated See https://developers.google.com/maps/premium/overview.
    */
   channel?: string;
   /**
@@ -231,6 +233,7 @@ export class Loader {
   private done = false;
   private loading = false;
   private onerrorEvent: Event;
+  private static instance: Loader;
 
   /**
    * Creates an instance of Loader using [[LoaderOptions]]. No defaults are set
@@ -265,6 +268,36 @@ export class Loader {
     this.mapIds = mapIds;
     this.nonce = nonce;
     this.url = url;
+
+    if (Loader.instance) {
+      if (!isEqual(this.options, Loader.instance.options)) {
+        throw new Error(
+          `Loader must not be called again with different options. ${JSON.stringify(
+            this.options
+          )} !== ${JSON.stringify(Loader.instance.options)}`
+        );
+      }
+
+      return Loader.instance;
+    }
+
+    Loader.instance = this;
+  }
+
+  get options(): LoaderOptions {
+    return {
+      version: this.version,
+      apiKey: this.apiKey,
+      channel: this.channel,
+      client: this.client,
+      id: this.id,
+      libraries: this.libraries,
+      language: this.language,
+      region: this.region,
+      mapIds: this.mapIds,
+      nonce: this.nonce,
+      url: this.url,
+    };
   }
   /**
    * CreateUrl returns the Google Maps JavaScript API script url given the [[LoaderOptions]].
@@ -348,6 +381,7 @@ export class Loader {
    */
   private setScript(): void {
     if (this.id && document.getElementById(this.id)) {
+      // TODO wrap onerror callback for cases where the script was loaded elsewhere
       this.callback();
       return;
     }
diff --git a/tsconfig.json b/tsconfig.json
index 5b210ceb..627d3dcb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,9 @@
     "outDir": "./dist",
     "sourceMap": true,
     "esModuleInterop": true,
-    "lib": ["DOM", "ESNext"]
+    "lib": ["DOM", "ESNext"],
+    "target": "ES6",
+    "moduleResolution": "node"
   },
   "include": ["src/**/*"]
 }