PERF: faster length calculation of composer body

PERF: faster length calculation of composer body

Every time we type a letter the composer issues a reply length check.

This is due to the interconnecting components, the title one depends on the body which means that this decision making is passed along each time, even if the title does not need it strictly.

Anyway…

This optimisation has 3 parts:

  1. If the composer string is super long (10000 chars) we will bypass, quote stripping and space squashing.

  2. Quote stripping is now done much more efficiently, we strip them all in one go

  3. Space squashing eg: hello SPACE world to hello world is done in an efficient loop to avoid needing to generate superflous strings that need GC

diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6
index 049bba2..d094917 100644
--- a/app/assets/javascripts/discourse/models/composer.js.es6
+++ b/app/assets/javascripts/discourse/models/composer.js.es6
@@ -56,7 +56,8 @@ const CLOSED = "closed",
     categoryId: "topic.category.id",
     tags: "topic.tags",
     featuredLink: "topic.featured_link"
-  };
+  },
+  FAST_REPLY_LENGTH_THRESHOLD = 10000;
 
 export const SAVE_LABELS = {
   [EDIT]: "composer.save_edit",
@@ -445,10 +446,62 @@ const Composer = RestModel.extend({
   @computed("reply")
   replyLength(reply) {
     reply = reply || "";
-    while (Quote.REGEXP.test(reply)) {
-      reply = reply.replace(Quote.REGEXP, "");
+
+    if (reply.length > FAST_REPLY_LENGTH_THRESHOLD) {
+      return reply.length;
+    }
+
+    if (Quote.REGEXP.test(reply)) {
+      // make it global so we can strip all quotes at once
+      const regex = new RegExp(Quote.REGEXP.source, "img");
+      reply = reply.replace(regex, "");
+    }
+
+    // This is in place so we do not generate any intermediate
+    // strings while calculating the length, this is issued
+    // every keypress in the composer so it needs to be very fast
+    let len = 0,
+      skipSpace = true;
+
+    for (let i = 0; i < reply.length; i++) {
+      const code = reply.charCodeAt(i);
+
+      let isSpace = false;
+      if (code >= 0x2000 && code <= 0x200a) {
+        isSpace = true;
+      } else {
+        switch (code) {
+          case 0x09: // \t
+          case 0x0a: // \n
+          case 0x0b: // \v
+          case 0x0c: // \f
+          case 0x0d: // \r
+          case 0x20:
+          case 0xa0:
+          case 0x1680:
+          case 0x202f:
+          case 0x205f:
+          case 0x3000:
+            isSpace = true;
+        }
+      }
+
+      if (isSpace) {
+        if (!skipSpace) {
+          len++;
+          skipSpace = true;
+        }
+      } else {
+        len++;
+        skipSpace = false;
+      }
     }
-    return reply.replace(/\s+/gim, " ").trim().length;
+
+    if (len > 0 && skipSpace) {
+      len--;
+    }
+
+    return len;
   },
 
   @on("init")

GitHub sha: afc7830b

1 Like

DEV: improvement to stripping quote logic