Optimizing the Frontend with Algorithms

#Editor#Algorithm#JavaScript

Why did I handle line breaks manually?

As I mentioned in a previous post, I worked on a text editor. At the company, we handled the editor's layout ourselves, and to do that we used the Canvas API.

Since we were doing layout ourselves, we also had to handle text line breaks manually. Text editors in particular have to react sensitively to user input and changes (like style updates), so line breaking has to happen instantly.

That meant every text render required calculating text width to figure out where lines should wrap.

This time, I want to share how I used algorithms in frontend development while optimizing that line-breaking logic.


I started simple

Linear search approach

Measuring text width with the Canvas API

The Canvas API I used was canvas.measureText(), which tells you the width of a piece of text.

const metrics = ctx.measureText('Hello, world');
console.log(metrics.width); // e.g. 72.5 (in pixels)

This API accurately calculates the pixel width the text will actually occupy based on the currently configured font style.

Initial linear search approach: At first, I checked the width by expanding the search range one character at a time linearly based on the input text.

// Original linear search approach (pseudocode)
function findLineBreakPoint(text, maxWidth) {
  for (let i = 1; i <= text.length; i++) {
    const partialText = text.substring(0, i);
    const width = canvas.measureText(partialText).width;
 
    if (width > maxWidth) {
      return i - 1; // The previous index is the max fitting point
    }
  }
  return text.length;
}

Then the VOCs started rolling in:

  • Users entering long text noticed increasing latency
  • Janky behavior when suddenly changing the style of long text
  • A continuous stream of performance issues related to long text in various forms

Analyzing the problem:

  • Calculation time grows linearly as text gets longer (O(n))
  • For a 100-character text, that's up to 100 measureText() calls in the worst case
  • In a real-time editing environment, this calculation repeats with every keystroke

I hit the limits of linear search. Especially for long text with hundreds or thousands of characters, the issue became something users could clearly feel.


Performance issues, and parametric search

Not binary search, but parametric search

My first instinct was to use binary search. But standard binary search is a search for a specific value, which is similar to but slightly off from the "find the longest prefix that fits within a given width" logic I needed.

What I actually needed was parametric search (English-speaking developers often call this "binary search on the answer").

Binary search vs parametric search

  • Binary search: "Find value X in the array"
  • Parametric search: "Use binary search to find the max/min value that satisfies a condition"

In my case, the goal was to find "the maximum number of characters that can fit within a given width."

Actual implementation:

function findLineBreakPoint(text, maxWidth) {
  let left = 0;
  let right = text.length;
  let result = 0;
 
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const partialText = text.substring(0, mid);
    const width = canvas.measureText(partialText).width;
 
    if (width <= maxWidth) {
      result = mid;  // Save the largest valid value so far
      left = mid + 1;  // Try longer text
    } else {
      right = mid - 1;  // Try shorter text
    }
  }
 
  return result;
}

How the core logic works:

  1. Compute the midpoint: Find mid and measure the text width up to that point
  2. Check the condition:
    • If width is within the allowed range → search the right half (try longer text)
    • If width exceeds the limit → search the left half (try shorter text)
  3. Save the optimal value: Continuously update the maximum value that satisfies the condition

Performance measurement process:

  • Tools: Browser DevTools' Performance tab
  • Test method: Held down a key for 10 seconds to continuously input text and measure
  • Comparison: Direct comparison of linear search vs parametric search

Results:

  • Performance improvement: 26.53ms → 13.41ms (50% improvement)
  • Time complexity: Improved from O(n) to O(log n)
  • Perceived effect: The longer the text, the more dramatic the difference
  • User VOC: Existing performance complaints were resolved

Is caching the answer? Why direct calculation was better

You might wonder, "Why not just cache it?" But unlike a viewer, where you just show the same characters, a text editor is a completely different situation.

Caching conditions were too complex in a real-time editing environment:

  1. Many variables:

    • Different font styles per character (bold, italic, size, etc.)
    • Text rotation angle
    • Character spacing, line spacing settings
  2. Cache key management complexity:

    // We would have needed a complex cache key like this
    const cacheKey = `${text}_${fontSize}_${fontFamily}_${fontWeight}_${rotation}_${letterSpacing}_...`;
  3. Various invalidation timings:

    • Every time the user edits text
    • Every time a style changes
    • Every time the layout changes
    • All related caches would need to be found and deleted
  4. Memory burden:

    • Storing countless combinations of text and style info
    • Need cache size limits and LRU management
    • Risk of memory leaks

In the end, I decided that calculating quickly in O(log n) was more efficient than implementing and maintaining complex caching logic.


When algorithms were actually needed in real work

I realized the idea that "algorithms are just for coding tests" is wrong.

This was a real-world case where user pain points → applying an algorithm → led to concrete performance improvements.

Seeing a small optimization change the overall usability made it clear that algorithm-based performance optimization matters in frontend development too.