diff --git a/src/components/App.tsx b/src/components/App.tsx
index 3f664865..1561b382 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -123,11 +123,7 @@ import {
 } from "../element/binding";
 import { LinearElementEditor } from "../element/linearElementEditor";
 import { mutateElement, newElementWith } from "../element/mutateElement";
-import {
-  deepCopyElement,
-  newFreeDrawElement,
-  refreshTextDimensions,
-} from "../element/newElement";
+import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
 import {
   hasBoundTextElement,
   isBindingElement,
@@ -272,6 +268,7 @@ import {
 } from "../element/Hyperlink";
 import { shouldShowBoundingBox } from "../element/transformHandles";
 import { atom } from "jotai";
+import { Fonts } from "../scene/Fonts";
 
 export const isMenuOpenAtom = atom(false);
 export const isDropdownOpenAtom = atom(false);
@@ -354,6 +351,7 @@ class App extends React.Component<AppProps, AppState> {
   };
 
   public scene: Scene;
+  private fonts: Fonts;
   private resizeObserver: ResizeObserver | undefined;
   private nearestScrollableContainer: HTMLElement | Document | undefined;
   public library: AppClassProperties["library"];
@@ -445,6 +443,10 @@ class App extends React.Component<AppProps, AppState> {
     };
 
     this.scene = new Scene();
+    this.fonts = new Fonts({
+      scene: this.scene,
+      onSceneUpdated: this.onSceneUpdated,
+    });
     this.history = new History();
     this.actionManager = new ActionManager(
       this.syncActionResult,
@@ -730,23 +732,6 @@ class App extends React.Component<AppProps, AppState> {
     event.preventDefault();
   };
 
-  private onFontLoaded = () => {
-    let didUpdate = false;
-    this.scene.mapElements((element) => {
-      if (isTextElement(element)) {
-        invalidateShapeForElement(element);
-        didUpdate = true;
-        return newElementWith(element, {
-          ...refreshTextDimensions(element),
-        });
-      }
-      return element;
-    });
-    if (didUpdate) {
-      this.onSceneUpdated();
-    }
-  };
-
   private resetHistory = () => {
     this.history.clear();
   };
@@ -846,6 +831,12 @@ class App extends React.Component<AppProps, AppState> {
       };
     }
 
+    // FontFaceSet loadingdone event we listen on may not always fire
+    // (looking at you Safari), so on init we manually load fonts for current
+    // text elements on canvas, and rerender them once done. This also
+    // seems faster even in browsers that do fire the loadingdone event.
+    this.fonts.loadFontsForElements(scene.elements);
+
     this.resetHistory();
     this.syncActionResult({
       ...scene,
@@ -1059,7 +1050,11 @@ class App extends React.Component<AppProps, AppState> {
       this.updateCurrentCursorPosition,
     );
     // rerender text elements on font load to fix #637 && #1553
-    document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
+    document.fonts?.addEventListener?.("loadingdone", (event) => {
+      const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
+      this.fonts.onFontsLoaded(loadedFontFaces);
+    });
+
     // Safari-only desktop pinch zoom
     document.addEventListener(
       EVENT.GESTURE_START,
diff --git a/src/global.d.ts b/src/global.d.ts
index 01464094..df7eeb37 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -2,6 +2,8 @@
 interface Document {
   fonts?: {
     ready?: Promise<void>;
+    check?: (font: string, text?: string) => boolean;
+    load?: (font: string, text?: string) => Promise<FontFace[]>;
     addEventListener?(
       type: "loading" | "loadingdone" | "loadingerror",
       listener: (this: Document, ev: Event) => any,
diff --git a/src/scene/Fonts.ts b/src/scene/Fonts.ts
new file mode 100644
index 00000000..cc206c77
--- /dev/null
+++ b/src/scene/Fonts.ts
@@ -0,0 +1,93 @@
+import { isTextElement, refreshTextDimensions } from "../element";
+import { newElementWith } from "../element/mutateElement";
+import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
+import { invalidateShapeForElement } from "../renderer/renderElement";
+import { getFontString } from "../utils";
+import type Scene from "./Scene";
+
+export class Fonts {
+  private scene: Scene;
+  private onSceneUpdated: () => void;
+
+  constructor({
+    scene,
+    onSceneUpdated,
+  }: {
+    scene: Scene;
+    onSceneUpdated: () => void;
+  }) {
+    this.scene = scene;
+    this.onSceneUpdated = onSceneUpdated;
+  }
+
+  // it's ok to track fonts across multiple instances only once, so let's use
+  // a static member to reduce memory footprint
+  private static loadedFontFaces = new Set<string>();
+
+  /**
+   * if we load a (new) font, it's likely that text elements using it have
+   * already been rendered using a fallback font. Thus, we want invalidate
+   * their shapes and rerender. See #637.
+   *
+   * Invalidates text elements and rerenders scene, provided that at least one
+   * of the supplied fontFaces has not already been processed.
+   */
+  public onFontsLoaded = (fontFaces: readonly FontFace[]) => {
+    if (
+      // bail if all fonts with have been processed. We're checking just a
+      // subset of the font properties (though it should be enough), so it
+      // can technically bail on a false positive.
+      fontFaces.every((fontFace) => {
+        const sig = `${fontFace.family}-${fontFace.style}-${fontFace.weight}`;
+        if (Fonts.loadedFontFaces.has(sig)) {
+          return true;
+        }
+        Fonts.loadedFontFaces.add(sig);
+        return false;
+      })
+    ) {
+      return false;
+    }
+
+    let didUpdate = false;
+
+    this.scene.mapElements((element) => {
+      if (isTextElement(element)) {
+        invalidateShapeForElement(element);
+        didUpdate = true;
+        return newElementWith(element, {
+          ...refreshTextDimensions(element),
+        });
+      }
+      return element;
+    });
+
+    if (didUpdate) {
+      this.onSceneUpdated();
+    }
+  };
+
+  public loadFontsForElements = async (
+    elements: readonly ExcalidrawElement[],
+  ) => {
+    const fontFaces = await Promise.all(
+      [
+        ...new Set(
+          elements
+            .filter((element) => isTextElement(element))
+            .map((element) => (element as ExcalidrawTextElement).fontFamily),
+        ),
+      ].map((fontFamily) => {
+        const fontString = getFontString({
+          fontFamily,
+          fontSize: 16,
+        });
+        if (!document.fonts?.check?.(fontString)) {
+          return document.fonts?.load?.(fontString);
+        }
+        return undefined;
+      }),
+    );
+    this.onFontsLoaded(fontFaces.flat().filter(Boolean) as FontFace[]);
+  };
+}