diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -503,10 +503,12 @@
       if (isFinalWrite) {
         Services.obs.notifyObservers(
           null,
           "sessionstore-final-state-write-complete"
         );
+
+        lazy.SessionWriter.deinit();
       }
     });
   },
 
   async wipe() {
diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs
--- a/browser/components/sessionstore/SessionWriter.sys.mjs
+++ b/browser/components/sessionstore/SessionWriter.sys.mjs
@@ -6,10 +6,12 @@
 
 ChromeUtils.defineESModuleGetters(lazy, {
   sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
 });
 
+const BROWSER_PURGE_SESSION_HISTORY = "browser:purge-session-history";
+
 /**
  * We just started (we haven't written anything to disk yet) from
  * `Paths.clean`. The backup directory may not exist.
  */
 const STATE_CLEAN = "clean";
@@ -58,10 +60,14 @@
 export const SessionWriter = {
   init(origin, useOldExtension, paths, prefs = {}) {
     return SessionWriterInternal.init(origin, useOldExtension, paths, prefs);
   },
 
+  deinit() {
+    return SessionWriterInternal.deinit();
+  },
+
   /**
    * Write the contents of the session file.
    *
    * @param state - May get changed on shutdown.
    */
@@ -80,10 +86,17 @@
       return await SessionWriterInternal.wipe();
     } finally {
       unlock();
     }
   },
+
+  /**
+   * *Test Only* Return the SessionWriter's length hint for writing JSON.
+   */
+  get _jsonLengthHint() {
+    return SessionWriterInternal.jsonLengthHint;
+  },
 };
 
 const SessionWriterInternal = {
   // Path to the files used by the SessionWriter
   Paths: null,
@@ -104,10 +117,19 @@
   /**
    * Number of old upgrade backups that are being kept
    */
   maxUpgradeBackups: null,
 
+  /**
+   * The size of the last write with IOUtils.writeJSON.
+   *
+   * Because SessionWriter writes such a large object graph we will otherwise
+   * spend a large portion of `write()` doing memory allocations and memcpy
+   * when serializing the session file to disk.
+   */
+  jsonLengthHint: 0,
+
   /**
    * Initialize (or reinitialize) the writer.
    *
    * @param {string} origin Which of sessionstore.js or its backups
    *   was used. One of the `STATE_*` constants defined above.
@@ -136,13 +158,20 @@
     this.Paths = paths;
     this.maxUpgradeBackups = prefs.maxUpgradeBackups;
     this.maxSerializeBack = prefs.maxSerializeBack;
     this.maxSerializeForward = prefs.maxSerializeForward;
     this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
+
+    Services.obs.addObserver(this, BROWSER_PURGE_SESSION_HISTORY);
+
     return { result: true };
   },
 
+  deinit() {
+    Services.obs.removeObserver(this, BROWSER_PURGE_SESSION_HISTORY);
+  },
+
   /**
    * Write the session to disk.
    * Write the session to disk, performing any necessary backup
    * along the way.
    *
@@ -208,36 +237,42 @@
         // We are shutting down. At this stage, we know that
         // $Paths.clean is either absent or corrupted. If it was
         // originally present and valid, it has been moved to
         // $Paths.cleanBackup a long time ago. We can therefore write
         // with the guarantees that we erase no important data.
-        await IOUtils.writeJSON(this.Paths.clean, state, {
+        const result = await IOUtils.writeJSON(this.Paths.clean, state, {
           tmpPath: this.Paths.clean + ".tmp",
           compress: true,
+          lengthHint: this.jsonLengthHint,
         });
+        this.jsonLengthHint = result.jsonLength;
         fileStat = await IOUtils.stat(this.Paths.clean);
       } else if (this.state == STATE_RECOVERY) {
         // At this stage, either $Paths.recovery was written >= 15
         // seconds ago during this session or we have just started
         // from $Paths.recovery left from the previous session. Either
         // way, $Paths.recovery is good. We can move $Path.backup to
         // $Path.recoveryBackup without erasing a good file with a bad
         // file.
-        await IOUtils.writeJSON(this.Paths.recovery, state, {
+        const result = await IOUtils.writeJSON(this.Paths.recovery, state, {
           tmpPath: this.Paths.recovery + ".tmp",
           backupFile: this.Paths.recoveryBackup,
           compress: true,
+          lengthHint: this.jsonLengthHint,
         });
+        this.jsonLengthHint = result.jsonLength;
         fileStat = await IOUtils.stat(this.Paths.recovery);
       } else {
         // In other cases, either $Path.recovery is not necessary, or
         // it doesn't exist or it has been corrupted. Regardless,
         // don't backup $Path.recovery.
-        await IOUtils.writeJSON(this.Paths.recovery, state, {
+        const result = await IOUtils.writeJSON(this.Paths.recovery, state, {
           tmpPath: this.Paths.recovery + ".tmp",
           compress: true,
+          lengthHint: this.jsonLengthHint,
         });
+        this.jsonLengthHint = result.jsonLength;
         fileStat = await IOUtils.stat(this.Paths.recovery);
       }
 
       telemetry.writeFileMs = Date.now() - startWriteMs;
       telemetry.fileSizeBytes = fileStat.size;
@@ -420,6 +455,18 @@
 
     if (exn) {
       throw exn;
     }
   },
+
+  observe(_subject, topic, _data) {
+    switch (topic) {
+      case BROWSER_PURGE_SESSION_HISTORY:
+        this._onPurgeSessionHistory();
+        break;
+    }
+  },
+
+  _onPurgeSessionHistory() {
+    this.jsonLengthHint = 0;
+  },
 };
diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { updateAppInfo } = ChromeUtils.importESModule(
+  "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const profile = do_get_profile();
+
+updateAppInfo({
+  name: "SessionRestoreTest",
+  ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+  version: "1",
+  platformVersion: "",
+});
+
+const { SessionFile } = ChromeUtils.importESModule(
+  "resource:///modules/sessionstore/SessionFile.sys.mjs"
+);
+const { SessionWriter } = ChromeUtils.importESModule(
+  "resource:///modules/sessionstore/SessionWriter.sys.mjs"
+);
+
+add_setup(async function setup() {
+  const source = do_get_file("data/sessionstore_valid.js");
+  source.copyTo(profile, "sessionstore.js");
+
+  await writeCompressedFile(
+    SessionFile.Paths.clean.replace("jsonlz4", "js"),
+    SessionFile.Paths.clean
+  );
+
+  await SessionFile.read();
+});
+
+add_task(async function test_json_length_hint() {
+  await IOUtils.writeJSON(PathUtils.join(PathUtils.profileDir, "dingus"), {
+    gunk: true,
+  });
+
+  Assert.equal(
+    SessionWriter._jsonLengthHint,
+    0,
+    "SessionWriter length hint starts at 0"
+  );
+
+  await SessionFile.write({});
+
+  const lengthHint = SessionWriter._jsonLengthHint;
+
+  Assert.equal(
+    SessionWriter._jsonLengthHint,
+    JSON.stringify({}).length,
+    "SessionWriter should cache length hint"
+  );
+
+  const contents = await IOUtils.readJSON(
+    PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json")
+  );
+  await SessionFile.write(contents);
+
+  Assert.notEqual(
+    SessionWriter._jsonLengthHint,
+    lengthHint,
+    "SessionWriter length hint updated"
+  );
+
+  Assert.greater(
+    SessionWriter._jsonLengthHint,
+    lengthHint,
+    "SessionWriteLength hint is now larger"
+  );
+
+  Services.obs.notifyObservers(null, "browser:purge-session-history");
+
+  Assert.equal(
+    SessionWriter._jsonLengthHint,
+    0,
+    "browser:purge-session-history notification cleans length hint"
+  );
+
+  await SessionFile.write(contents);
+
+  Assert.notEqual(
+    SessionWriter._jsonLengthHint,
+    lengthHint,
+    "SessionWriter length hint updated"
+  );
+});
diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml
--- a/browser/components/sessionstore/test/unit/xpcshell.toml
+++ b/browser/components/sessionstore/test/unit/xpcshell.toml
@@ -39,5 +39,7 @@
 skip-if = [
   "condprof", # Bug 1769154
 ]
 
 ["test_startup_session_async.js"]
+
+["test_write_json_length_hint.js"]

