<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gal Be</title>
    <description>The latest articles on DEV Community by Gal Be (@galb).</description>
    <link>https://dev.to/galb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4127795%2F5f3923e9-3ec7-4a25-b677-2945e1352172.png</url>
      <title>DEV Community: Gal Be</title>
      <link>https://dev.to/galb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/galb"/>
    <language>en</language>
    <item>
      <title>I made i18next up to 5x faster with two includes() checks</title>
      <dc:creator>Gal Be</dc:creator>
      <pubDate>Thu, 12 Nov 2026 14:37:38 +0000</pubDate>
      <link>https://dev.to/galb/i-made-i18next-up-to-5x-faster-with-two-includes-checks-j6n</link>
      <guid>https://dev.to/galb/i-made-i18next-up-to-5x-faster-with-two-includes-checks-j6n</guid>
      <description>&lt;p&gt;Two &lt;code&gt;includes()&lt;/code&gt; checks made i18next up to 5x faster on long text. This post shows that fix and two more.&lt;/p&gt;

&lt;p&gt;A regular expression is a good tool, but it is not free. Each call scans the input, and a bad pattern can scan the same characters many times.&lt;/p&gt;

&lt;p&gt;Three of my performance PRs had the same idea: &lt;strong&gt;do a cheap check before the expensive regex work&lt;/strong&gt;. Maintainers merged all three. This post shows each change, the numbers, and the rule you can use in your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. i18next: check the prefix before you run the regex
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/i18next/i18next" rel="noopener noreferrer"&gt;i18next&lt;/a&gt; (35M npm downloads a week) runs its interpolation regex and its nesting regex on every translated text. The regexes look for &lt;code&gt;{{name}}&lt;/code&gt; and &lt;code&gt;$t(key)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most texts have neither. "Save", "Cancel" and "Welcome back" contain no &lt;code&gt;{{&lt;/code&gt; and no &lt;code&gt;$t(&lt;/code&gt;. But i18next ran both regexes on them anyway, and each scan read the full text to find nothing.&lt;/p&gt;

&lt;p&gt;Every match of these regexes starts with a fixed prefix. So a text without the prefix cannot match. A plain &lt;code&gt;includes()&lt;/code&gt; call answers that question, and it is much cheaper than a regex scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Interpolator.interpolate()&lt;/span&gt;
&lt;span class="c1"&gt;// no placeholder at all: neither regexp can match&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefixText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Interpolator.nest()&lt;/span&gt;
&lt;span class="c1"&gt;// nothing to nest (the common case)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nestingPrefixText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prefixText&lt;/code&gt; is &lt;code&gt;{{&lt;/code&gt; by default, and &lt;code&gt;nestingPrefixText&lt;/code&gt; is &lt;code&gt;$t(&lt;/code&gt;. Both follow the user's options, so a custom prefix such as &lt;code&gt;%{&lt;/code&gt; also works.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t('hello')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1524 ns&lt;/td&gt;
&lt;td&gt;1310 ns&lt;/td&gt;
&lt;td&gt;-14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;t()&lt;/code&gt; on a 10 KB text&lt;/td&gt;
&lt;td&gt;11858 ns&lt;/td&gt;
&lt;td&gt;2232 ns&lt;/td&gt;
&lt;td&gt;-81% (5.3x faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;t()&lt;/code&gt; on a 10 KB text with &lt;code&gt;{{name}}&lt;/code&gt; at the end&lt;/td&gt;
&lt;td&gt;12431 ns&lt;/td&gt;
&lt;td&gt;7250 ns&lt;/td&gt;
&lt;td&gt;-42%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The long text gets the biggest gain, because the regex scan was most of the work. The output does not change. Old and new code gave the same results on 1,000,000 random resources, options and calls.&lt;/p&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/i18next/i18next/pull/2469" rel="noopener noreferrer"&gt;i18next #2469&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. regexp2: check ASCII before the Unicode tables
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/dlclark/regexp2" rel="noopener noreferrer"&gt;regexp2&lt;/a&gt; is a .NET-style regex engine for Go. The &lt;code&gt;\w&lt;/code&gt; and &lt;code&gt;\b&lt;/code&gt; tokens call &lt;code&gt;IsWordChar&lt;/code&gt; for each character. That function looked up 4 Unicode category tables by name, on every call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;In&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"L"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Mn"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Nd"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Pc"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'\u200D'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'\u200C'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things were slow. First, &lt;code&gt;unicode.Categories["L"]&lt;/code&gt; is a map lookup, 4 times per character. Second, most text in logs, code and config files is ASCII. In ASCII, these 4 categories are only letters, digits and &lt;code&gt;_&lt;/code&gt;. The change checks ASCII first, and uses the table variables directly for the other runes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RuneSelf&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In ASCII, L, Mn, Nd and Pc are these runes only.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'a'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'z'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'Z'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'9'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;In&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Mn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Nd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'\u200D'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'\u200C'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark (run time)&lt;/th&gt;
&lt;th&gt;Apple M5&lt;/th&gt;
&lt;th&gt;AMD EPYC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compile an email pattern&lt;/td&gt;
&lt;td&gt;-69 to -71%&lt;/td&gt;
&lt;td&gt;-68 to -69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;\b\w+\b&lt;/code&gt; on ASCII log text&lt;/td&gt;
&lt;td&gt;-44 to -48%&lt;/td&gt;
&lt;td&gt;-45 to -49%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Non-ASCII text also got 33 to 37% faster in my test, because the map lookups are gone for all runes. I checked the result for every rune.&lt;/p&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/dlclark/regexp2/pull/142" rel="noopener noreferrer"&gt;regexp2 #142&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. colord: let the common case match first
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/omgovich/colord" rel="noopener noreferrer"&gt;colord&lt;/a&gt; (25M npm downloads a week) parses CSS colors such as &lt;code&gt;rgba(255, 128, 0, 0.5)&lt;/code&gt;. Each number in its regexes used this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt;\&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;\&lt;span class="p"&gt;.&lt;/span&gt;\&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+|&lt;/span&gt;\&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;   &lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most numbers in CSS colors are integers, such as &lt;code&gt;255&lt;/code&gt;. For an integer, the first branch reads all the digits, finds no &lt;code&gt;.&lt;/code&gt;, and gives the digits back one by one. Then the second branch reads the same digits again.&lt;/p&gt;

&lt;p&gt;The new pattern accepts the same strings (&lt;code&gt;.5&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;, &lt;code&gt;5.5&lt;/code&gt;), but an integer matches in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt;\&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt;\&lt;span class="p"&gt;.&lt;/span&gt;\&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?|&lt;/span&gt;\&lt;span class="p"&gt;.&lt;/span&gt;\&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;   &lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;colord("rgba(255, 128, 0, 0.5)").toRgb()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;245 ns&lt;/td&gt;
&lt;td&gt;222 ns&lt;/td&gt;
&lt;td&gt;-9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gain is smaller here, but it costs nothing. colord also has a test that rejects 1 MB malformed strings in linear time. The new pattern passes it, and it is about 2x faster on some of those cases.&lt;/p&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/omgovich/colord/pull/173" rel="noopener noreferrer"&gt;colord #173&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;All three changes do the same thing: they find the cheapest question that settles the common case, and ask it first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fixed prefix?&lt;/strong&gt; Check it with &lt;code&gt;includes()&lt;/code&gt; or &lt;code&gt;strings.Contains()&lt;/code&gt; before you run the regex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mostly ASCII input?&lt;/strong&gt; Handle ASCII with a range check before you go to the Unicode tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alternatives in a pattern?&lt;/strong&gt; Put the branch that matches the common input first, and make sure it does not backtrack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When does this not help? If almost every input contains the prefix, the extra check is only extra work. Measure with your real inputs before you add it.&lt;/p&gt;

&lt;p&gt;The full benchmarks are in each PR. More PRs: &lt;a href="https://github.com/g4lb" rel="noopener noreferrer"&gt;g4lb&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>go</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
