The Long Road to a Faster Escape Function
Dropbox serves a large number of dynamically generated pages, which means every piece of user-generated text has to be escaped before it reaches a browser. That escaping is context-sensitive: text that is harmless in an HTML attribute may be dangerous inside a JavaScript string, and vice versa. So the engineering team built a custom escaping function that handles these different contexts. This article walks through the optimization of the HTML text context of that function, which saw rendering speeds improve by roughly 5x over the course of a summer internship.
The standard library's xml.sax.saxutils.escape and cgi.escape were not up to the job. They don't escape enough characters for all contexts; a browser could even be tricked into misinterpreting a page as UTF-7, whichturns the equals sign into a necessary escape. So the custom function is a requirement, not a choice. The original implementation, written for readability over speed, made heavy use of function calls and immediately looked like a target for optimization.
Inlining for Immediate Gains
The first version called an escaping function for every single character in the string, a notoriously slow pattern in Python. Even worse, in the most common case, an ASCII character that didn't need escaping, the function was called and did nothing. The initial fix inlined the per-character logic directly into the loop and merged the separate ASCII and Unicode paths. This immediately shaved about 15 percent off the runtime.
Going Loopy Over Loop Overhead
The inlined function still used a Python loop to iterate character by character. The next attempt swapped the explicit for loop for a generator expression, a structure that is often more readable. However, the timing results went in the wrong direction: the generator expression version was slower than the inlined loop. The culprit is that a generator expression constructs a generator object, and that construction overhead is not free.
Switching from a generator expression to a list comprehension fixed the regression and made the function faster than it had been at any prior point. The explanation is that the sample strings were likely too short to benefit from a generator's laziness, and the list comprehension avoided the generator object's overhead entirely.
Sets, Locals, and String Building
The code was still performing a linear search against a whitelist string on every single character. str.__contains__ is an efficient implementation but still runs in linear time. Replacing the string with a set turned each lookup into a constant-time operation and resulted in a large, immediate improvement.
Next up was a standard Python micro-optimization: binding frequently used global variables to local names. There are two LOAD instructions in CPython (LOAD_GLOBAL and LOAD_FAST), and fetching a local variable is significantly cheaper. The function under test was still referencing the global ord builtin and the WHITELIST constant repeatedly. Localizing these didn't buy much in absolute terms but was still a meaningful percentage gain.
The follow-up optimization was more surprising. The code was building escaped output using string interpolation, and profiling indicated that this was a bottleneck. Just replacing the f-string style construction with simple concatenation resulted in a huge boost, saving more than a second of processing time. Even a seemingly simple computation like converting a character's code point to a string with str(ord(c)) has tangible costs.
A Hash Table for Everything
String concatenation was still per-character work. Rather than doing any per-character computation, why not cache the fully escaped form of each character we've seen before?
The code next built a caching dictionary, CACHE_HTML_ESCAPES, that mapped a character to the string that represents its escaped HTML entity. On each character, the function attempts a cache lookup. Python's dicts are exceptionally fast, and relying on them sidestepped the cost of str(ord(c)) and the concatenation logic. The read path used setdefault, which was also dict-optimized code. This produced another major speedup. Since web servers are long-running, the cache fills up with the characters actually used by real traffic.
The code still used two globals in the rare cache-allocation branch: str and ord. Given how rarely those are executed, making them local variables was determined not to be worth the code clutter. This held up in testing, yielding only a negligible change.
Bringing C Into the Picture
At this point, all the per-character work involved Python-level operations. The standard advice in Python performance work is to push the heavy lifting into C where possible. The regular expression module, re, is a prime candidate. The whole operation could be reframed: find any single character not in the allowed set and swap it out for its escaped entity. For the common case of a purely alphanumeric string, this means the entire search happens in C, with almost no per-character overhead in Python.
This regex-based solution posted a small win over the previous champion on general data but was not a slam dunk. However, performance on a whitelisted-only, purely alphanumeric input was remarkable, proving that the C-based scanning had virtually no per-character cost.
The hidden cost of this approach is that re's search methods return match objects, and retrieving the full character that was matched requires a call to the group method. That extra function call makes the regex approach substantially slower on punctuation-heavy inputs, where it has to repeatedly perform expensive regex matches instead of simple dictionary lookups.
Choosing a Strategy at Runtime
The stats tell a clear story: the pure-Python function is faster for punctuation-heavy and shorter strings, while the regex function excels on long, mostly-ASCII English text. The initial hybrid solution attempted to predict the best function by examining the length-based proportion of digits and letters in the string against a few thresholds. In benchmarks, it worked as hoped for the extremes but suffered from a painful 20 to 30 percent overhead in the marginal cases where the string was right at the decision boundary.
A smarter check was to sample only the first 10 or so characters. Sampling truncated further than a full scan had a similar identifying power and kept costs down, making it a viable production approach despite some overhead. The final tactic was entirely different: skip the per-request detection and make the choice ahead of time based on user locale. Since a lot of the international users tend toward non-ASCII strings, using the pure-Python, dictionary-based version for those users and the fast regex version for users in mostly-Latin regions removed the checking overhead altogether. The hyphenated and non-Latin character-heavy output of international users now hits the list-comprehension-based algorithm html9, while the htmlA regex engine is parked beside the alphanumerics. This approach avoids the cost of the runtime dispatch and maximized the advantage of C-level speed in the most common case.
Not Every Stone Was Turned
Two obvious further optimizations were rejected in the name of maintainability. Re-implementing the whole function as a C extension could yield the maximum performance but would make the code significantly harder to own and sustain. Likewise, running this pure-Python version under a JIT compiler like PyPy would likely close most of the remaining gap to the regex implementation, but that is an infrastructure decision beyond the scope of a single function.
The clearest lesson from this project is that some common Python wisdom holds up, and some does not. Inlining functions and pushing loops towards implicit, C-backed constructs offer solid, predictable wins. Making local copies of global functions is a reliable but tiny victory. The staggering win in this exercise was discovering that string interpolation was horrifically slow, something no one would have predicted without measurement. A good dictionary, backed by a solid hash table, is one of the best tools available
Ultimately, the most useful thing was being on stage with a stopwatch. Only by measuring each substitution—from in over a string to a set membership test, from the % operator to plain +, from generator objects to list comprehensions—was it possible to tell what actually mattered. It turned out that the slow features were not the obviously suspicious ones.



