diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl
--- a/dom/chrome-webidl/IOUtils.webidl
+++ b/dom/chrome-webidl/IOUtils.webidl
@@ -94,23 +94,23 @@
    *         otherwise rejects with a DOMException.
    */
   [NewObject]
   Promise<unsigned long long> writeUTF8(DOMString path, UTF8String string, optional WriteOptions options = {});
   /**
-   * Attempts to serialize |value| into a JSON string and encode it as into a
-   * UTF-8 string, then safely write the result to a file at |path|. Works
-   * exactly like |write|.
+   * Attempts to serialize |value| into a JSON string and encode it as a UTF-8
+   * string, then safely write the result to a file at |path|. Works exactly
+   * like |write|.
    *
    * @param path      An absolute file path
    * @param value     The value to be serialized.
    * @param options   Options for writing the file. The "append" mode is not supported.
    *
    * @return Resolves with the number of bytes successfully written to the file,
    *         otherwise rejects with a DOMException.
    */
   [NewObject]
-  Promise<unsigned long long> writeJSON(DOMString path, any value, optional WriteOptions options = {});
+  Promise<WriteJSONResult> writeJSON(DOMString path, any value, optional WriteJSONOptions options = {});
   /**
    * Moves the file from |sourcePath| to |destPath|, creating necessary parents.
    * If |destPath| is a directory, then the source file will be moved into the
    * destination directory.
    *
@@ -567,10 +567,39 @@
    * If true, compress the data with LZ4-encoding before writing to the file.
    */
   boolean compress = false;
 };
 
+/**
+ * Options to be passed to the |IOUtils.writeJSON| method.
+ */
+dictionary WriteJSONOptions: WriteOptions {
+  /**
+   * An optional length hint that will be used to pre-allocate the buffer that
+   * will hold the stringified JSON.
+   *
+   * This is the *length* and not the size (i.e., it is the number of UTF-16
+   * codepoints and not the number of bytes).
+   */
+  unsigned long long lengthHint = 0;
+};
+
+/**
+ * Information about a WriteJSON operation.
+ */
+dictionary WriteJSONResult {
+  /**
+   * The number of bytes written.
+   */
+  required unsigned long long size;
+
+  /**
+   * The length of the stringified JSON (in UTF-16 codepoints).
+   */
+  required unsigned long long jsonLength;
+};
+
 /**
  * Options to be passed to the |IOUtils.move| method.
  */
 dictionary MoveOptions {
   /**
diff --git a/xpcom/ioutils/IOUtils.h b/xpcom/ioutils/IOUtils.h
--- a/xpcom/ioutils/IOUtils.h
+++ b/xpcom/ioutils/IOUtils.h
@@ -94,11 +94,11 @@
       const nsACString& aString, const dom::WriteOptions& aOptions,
       ErrorResult& aError);
 
   static already_AddRefed<dom::Promise> WriteJSON(
       dom::GlobalObject& aGlobal, const nsAString& aPath,
-      JS::Handle<JS::Value> aValue, const dom::WriteOptions& aOptions,
+      JS::Handle<JS::Value> aValue, const dom::WriteJSONOptions& aOptions,
       ErrorResult& aError);
 
   static already_AddRefed<dom::Promise> Move(dom::GlobalObject& aGlobal,
                                              const nsAString& aSourcePath,
                                              const nsAString& aDestPath,
@@ -736,13 +736,16 @@
   RefPtr<nsIFile> mBackupFile;
   RefPtr<nsIFile> mTmpFile;
   dom::WriteMode mMode;
   bool mFlush = false;
   bool mCompress = false;
+  size_t mLengthHint = 0;
 
   static Result<InternalWriteOpts, IOUtils::IOError> FromBinding(
       const dom::WriteOptions& aOptions);
+  static Result<InternalWriteOpts, IOUtils::IOError> FromBinding(
+      const dom::WriteJSONOptions& aOptions);
 };
 
 /**
  * Re-implements the file compression and decompression utilities found
  * in toolkit/components/lz4/lz4.js
diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp
--- a/xpcom/ioutils/IOUtils.cpp
+++ b/xpcom/ioutils/IOUtils.cpp
@@ -589,15 +589,21 @@
               return WriteSync(file, AsBytes(Span(str)), opts);
             });
       });
 }
 
+static bool AppendJSON(const char16_t* aBuf, uint32_t aLen, void* aStr) {
+  nsAString* str = static_cast<nsAString*>(aStr);
+
+  return str->Append(aBuf, aLen, fallible);
+}
+
 /* static */
 already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal,
                                              const nsAString& aPath,
                                              JS::Handle<JS::Value> aValue,
-                                             const WriteOptions& aOptions,
+                                             const WriteJSONOptions& aOptions,
                                              ErrorResult& aError) {
   return WithPromiseAndState(
       aGlobal, aError, [&](Promise* promise, auto& state) {
         nsCOMPtr<nsIFile> file = new nsLocalFile();
         REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
@@ -623,14 +629,15 @@
                               file->HumanReadablePath().get()));
           return;
         }
 
         JSContext* cx = aGlobal.Context();
-        JS::Rooted<JS::Value> rootedValue(cx, aValue);
+        JS::Rooted<JS::Value> value(cx, aValue);
         nsString string;
-        if (!nsContentUtils::StringifyJSON(cx, aValue, string,
-                                           UndefinedIsNullStringLiteral)) {
+        if (!JS_StringifyWithLengthHint(cx, &value, nullptr,
+                                        JS::NullHandleValue, AppendJSON,
+                                        &string, opts.mLengthHint)) {
           JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue());
           if (JS_GetPendingException(cx, &exn)) {
             JS_ClearPendingException(cx);
             promise->MaybeReject(exn);
           } else {
@@ -639,22 +646,29 @@
                                     "Could not serialize object to JSON"_ns));
           }
           return;
         }
 
-        DispatchAndResolve<uint32_t>(
+        DispatchAndResolve<dom::WriteJSONResult>(
             state->mEventQueue, promise,
             [file = std::move(file), string = std::move(string),
-             opts = std::move(opts)]() -> Result<uint32_t, IOError> {
+             opts = std::move(opts)]() -> Result<WriteJSONResult, IOError> {
               nsAutoCString utf8Str;
               if (!CopyUTF16toUTF8(string, utf8Str, fallible)) {
                 return Err(IOError(
                     NS_ERROR_OUT_OF_MEMORY,
                     "Failed to write to `%s': could not allocate buffer",
                     file->HumanReadablePath().get()));
               }
-              return WriteSync(file, AsBytes(Span(utf8Str)), opts);
+
+              uint32_t size =
+                  MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts));
+
+              dom::WriteJSONResult result;
+              result.mSize = size;
+              result.mJsonLength = static_cast<uint32_t>(string.Length());
+              return result;
             });
       });
 }
 
 /* static */
@@ -2840,10 +2854,20 @@
 
   opts.mCompress = aOptions.mCompress;
   return opts;
 }
 
+Result<IOUtils::InternalWriteOpts, IOUtils::IOError>
+IOUtils::InternalWriteOpts::FromBinding(const WriteJSONOptions& aOptions) {
+  InternalWriteOpts opts =
+      MOZ_TRY(FromBinding(static_cast<const WriteOptions&>(aOptions)));
+
+  opts.mLengthHint = aOptions.mLengthHint;
+
+  return opts;
+}
+
 /* static */
 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::JsBuffer::Create(
     IOUtils::BufferKind aBufferKind, size_t aCapacity) {
   JsBuffer buffer(aBufferKind, aCapacity);
   if (aCapacity != 0 && !buffer.mBuffer) {
diff --git a/xpcom/ioutils/tests/test_ioutils_read_write_json.html b/xpcom/ioutils/tests/test_ioutils_read_write_json.html
--- a/xpcom/ioutils/tests/test_ioutils_read_write_json.html
+++ b/xpcom/ioutils/tests/test_ioutils_read_write_json.html
@@ -140,10 +140,43 @@
       );
 
       await cleanup(filename);
     });
 
+    add_task(async function test_writeJSON_return() {
+      const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_writeJSON_return.tmp");
+
+      const obj = { emoji: "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐" };
+
+      const expectedJson = JSON.stringify(obj);
+      const size = new TextEncoder().encode(expectedJson).byteLength;
+
+      {
+        const result = await IOUtils.writeJSON(filename, obj, { lengthHint: 0 });
+
+        is(await IOUtils.readUTF8(filename), expectedJson, "should have written expected JSON");
+
+        is(typeof result, "object", "writeJSON returns an object");
+        ok(result !== null, "writeJSON returns non-null");
+
+        ok(Object.hasOwn(result, "size"), "result has size property");
+        ok(Object.hasOwn(result, "jsonLength"), "result has jsonLength property");
+
+        is(result.size, size, "Should have written the expected number of bytes");
+        is(result.jsonLength, expectedJson.length, "Should have written the expected number of UTF-16 codepoints");
+      }
+
+      {
+        const result = await IOUtils.writeJSON(filename, obj, { lengthHint: expectedJson.length, compress: true });
+
+        isnot(result.size, size, "Should have written a different number of bytes due to compression");
+        is(result.jsonLength, expectedJson.length, "Should have written the same number of UTF-16 codepoints");
+      }
+
+      await cleanup(filename);
+    });
+
     add_task(async function test_append_json() {
       const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp");
 
       await IOUtils.writeJSON(filename, OBJECT);
 

