<?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: Сергей С</title>
    <description>The latest articles on DEV Community by Сергей С (@__2a8c2d60e86a10a2).</description>
    <link>https://dev.to/__2a8c2d60e86a10a2</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%2F4215149%2Fde0bd8a8-905c-4414-9237-9fb383eba76c.png</url>
      <title>DEV Community: Сергей С</title>
      <link>https://dev.to/__2a8c2d60e86a10a2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__2a8c2d60e86a10a2"/>
    <language>en</language>
    <item>
      <title>Building an RSVP speed-reading Chrome extension: a red letter, pauses and very long words</title>
      <dc:creator>Сергей С</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:57:46 +0000</pubDate>
      <link>https://dev.to/__2a8c2d60e86a10a2/building-an-rsvp-speed-reading-chrome-extension-a-red-letter-pauses-and-very-long-words-3j87</link>
      <guid>https://dev.to/__2a8c2d60e86a10a2/building-an-rsvp-speed-reading-chrome-extension-a-red-letter-pauses-and-very-long-words-3j87</guid>
      <description>&lt;p&gt;I read a lot on screen and mostly skim. So I built a small Chrome extension, &lt;strong&gt;ListoRead&lt;/strong&gt;, that shows any text one word at a time in a single spot (RSVP, rapid serial visual presentation). The idea is about fifty years old and looks trivial: take a word, show it, wait, next. In practice almost all the work is in details you only notice when you read with it yourself. Here they are, with code. Plain JavaScript, no libraries, Manifest V3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the eye should land
&lt;/h2&gt;

&lt;p&gt;Centering each word feels jittery: short and long words start in different places and your eye keeps hunting. The fix is the &lt;em&gt;optimal recognition point&lt;/em&gt; (ORP), a letter slightly left of the middle. ListoRead colors it red and pins it to the same spot for every word.&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="cm"&gt;/** ORP index (0-based) for a word with `length` letters. */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrpIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Punctuation doesn't count.&lt;/strong&gt; In &lt;code&gt;"Hello,"&lt;/code&gt; the quotes and comma must not shift the red letter, so the index is computed on letters only and edge punctuation goes to a prefix/suffix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letters are graphemes, not code units.&lt;/strong&gt; Emoji and combining accents break &lt;code&gt;str[i]&lt;/code&gt;. Words are split with &lt;code&gt;Intl.Segmenter&lt;/code&gt;, otherwise you sometimes color half a character.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rhythm matters more than speed
&lt;/h2&gt;

&lt;p&gt;A flat interval feels like a metronome. Each word gets a multiplier on the base interval:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMMA_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// , ; : — –&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SENTENCE_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// . ! ? …&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARAGRAPH_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// line break after the word&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHORT_WORD_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1–3 letters&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LONG_WORD_FROM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// letters; from here each letter adds LONG_WORD_STEP&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LONG_WORD_STEP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LONG_WORD_MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final delay is the length factor times the punctuation factor. One more small thing: after Play, speed ramps from 60% to 100% over the first 8 words, so the first word doesn't fly past before your eyes focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Very long words
&lt;/h2&gt;

&lt;p&gt;English rarely has this problem; German and Russian have it all the time. Words of 14+ letters are split with a hyphen. Existing hyphens are used first; otherwise the cut is chosen near the middle, preferring vowel|consonant, then consonant|consonant, then vowel|vowel, with parts at least 4 and 3 letters long. URLs, emails, paths and long numbers are never split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the article when nothing is selected
&lt;/h2&gt;

&lt;p&gt;The main flow is: open an article, press Alt+R. No Readability, just a short heuristic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take visible &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; elements longer than 40 characters.&lt;/li&gt;
&lt;li&gt;Skip anything inside &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;aside&lt;/code&gt;, &lt;code&gt;footer&lt;/code&gt;, &lt;code&gt;form&lt;/code&gt;, comments, sidebars, share and related blocks.&lt;/li&gt;
&lt;li&gt;Add each paragraph's length to its parent in full and to its grandparent at half.&lt;/li&gt;
&lt;li&gt;The highest-scoring element wins; collect headings, paragraphs, lists and quotes from it in order.&lt;/li&gt;
&lt;li&gt;Under 200 characters means "no article here", and the extension says so.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works well on news sites, blogs and docs, and fails on pages split into dozens of tiny containers. There you select the text by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting back to where you stopped
&lt;/h2&gt;

&lt;p&gt;RSVP's honest weakness is that you can't glance back. So pause shows the whole sentence and you can click any word to jump to it. When you close the reader, the page scrolls to the word where you stopped and highlights it. The reader text and the page text differ (hidden nodes, markup, removed junk), so the match is searched within ~80 words of the proportional position, preferring candidates whose neighbours also match.&lt;/p&gt;

&lt;p&gt;The highlight uses the CSS Custom Highlight API: a &lt;code&gt;Range&lt;/code&gt; registered in &lt;code&gt;CSS.highlights&lt;/code&gt;. No elements are added to the page, nothing breaks the site's layout, and it disappears after 15 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation and permissions
&lt;/h2&gt;

&lt;p&gt;The reader renders in a &lt;strong&gt;closed Shadow DOM&lt;/strong&gt;. Permissions are &lt;code&gt;activeTab&lt;/code&gt;, &lt;code&gt;scripting&lt;/code&gt;, &lt;code&gt;contextMenus&lt;/code&gt; and &lt;code&gt;storage&lt;/code&gt;, with no &lt;code&gt;host_permissions&lt;/code&gt;: the extension touches a page only when you start reading. There is no server; settings live in &lt;code&gt;chrome.storage.local&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;ListoRead is free, has 11 UI languages and works in Chrome, Edge, Brave and other Chromium browsers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/ibcmjbncccejclcbaocnibllppkmkkpi?utm_source=devto" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/ibcmjbncccejclcbaocnibllppkmkkpi?utm_source=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;No install: &lt;a href="https://listoread.com/reader?utm_source=devto" rel="noopener noreferrer"&gt;https://listoread.com/reader?utm_source=devto&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love feedback on the article-detection heuristic and the word-splitting rules. If you know sites where detection breaks, tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with AI assistance and reviewed by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
