<?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: wajid ali</title>
    <description>The latest articles on DEV Community by wajid ali (@wajid_ali_b0a0b0548ce555b).</description>
    <link>https://dev.to/wajid_ali_b0a0b0548ce555b</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%2F4237762%2F8a6d9fb9-fcbc-4707-bda5-402a3e5e4700.png</url>
      <title>DEV Community: wajid ali</title>
      <link>https://dev.to/wajid_ali_b0a0b0548ce555b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wajid_ali_b0a0b0548ce555b"/>
    <language>en</language>
    <item>
      <title>Running Stockfish in the browser: how I built a free chess move finder with zero server cost</title>
      <dc:creator>wajid ali</dc:creator>
      <pubDate>Tue, 10 Nov 2026 19:50:37 +0000</pubDate>
      <link>https://dev.to/wajid_ali_b0a0b0548ce555b/running-stockfish-in-the-browser-how-i-built-a-free-chess-move-finder-with-zero-server-cost-4g31</link>
      <guid>https://dev.to/wajid_ali_b0a0b0548ce555b/running-stockfish-in-the-browser-how-i-built-a-free-chess-move-finder-with-zero-server-cost-4g31</guid>
      <description>&lt;p&gt;Most "find the best chess move" websites work the same way: you send a position to their server, an engine analyses it there, and they pay for every second of CPU. That's why so many of them cap free analysis per day or fill the page with ads.&lt;/p&gt;

&lt;p&gt;When I built &lt;a href="https://chessmovefinder.com" rel="noopener noreferrer"&gt;Chess Move Finder&lt;/a&gt;, I went the other way. &lt;strong&gt;Stockfish 17.1 runs entirely in the user's browser.&lt;/strong&gt; The server only serves static files. Analysis costs me nothing per user, so the tool can stay free with no account, no ads and no daily limit.&lt;/p&gt;

&lt;p&gt;Here's how it works and the tradeoffs I ran into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js (App Router)&lt;/strong&gt; for the site, tools, blog and translated pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stockfish 17.1, compiled to WebAssembly&lt;/strong&gt;, using the &lt;em&gt;lite, single-threaded&lt;/em&gt; build&lt;/li&gt;
&lt;li&gt;The engine runs in a &lt;strong&gt;Web Worker&lt;/strong&gt;, so the board and UI never freeze while it thinks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the "lite single-threaded" build
&lt;/h2&gt;

&lt;p&gt;Stockfish's WebAssembly builds come in a few flavours, and the choice matters more than I expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-threaded builds need SharedArrayBuffer.&lt;/strong&gt; That means the page must be &lt;em&gt;cross-origin isolated&lt;/em&gt;: you have to send &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt; and &lt;code&gt;Cross-Origin-Embedder-Policy&lt;/code&gt; headers. That's fine for a single-page app, but it breaks a lot of normal things on a content site: third-party embeds, some analytics, images from other origins. My site has a blog and other tools alongside the engine, so I didn't want those headers everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The single-threaded build needs none of that.&lt;/strong&gt; It works on any page, in any modern browser, on any phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Lite" means a smaller neural network (NNUE) file.&lt;/strong&gt; The full network is large; the lite one is a fraction of the size. It plays slightly weaker, but it's still far stronger than any human, and the first analysis starts much faster, especially on mobile data. For a "what's the best move here?" tool, a fast first answer matters more than the last few Elo points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Talking to the engine: UCI over postMessage
&lt;/h2&gt;

&lt;p&gt;Stockfish speaks &lt;strong&gt;UCI&lt;/strong&gt; (Universal Chess Interface), a plain-text protocol. Inside a Web Worker, you send it strings and listen for strings back:&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="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/engine/stockfish.js'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&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="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'info'&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="n"&gt;line&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="s1"&gt;' pv '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// depth, score (cp or mate), and the principal variation&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="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bestmove'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "e2e4"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'uci'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'setoption name MultiPV value 3'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// top 3 lines&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'position fen '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'go movetime 1500'&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;A few things worth getting right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wait for &lt;code&gt;uciok&lt;/code&gt; / &lt;code&gt;readyok&lt;/code&gt;&lt;/strong&gt; before sending a position, especially on slow devices where the WASM takes a moment to initialise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always send &lt;code&gt;stop&lt;/code&gt; before a new search.&lt;/strong&gt; Users click fast. Without it, results from an old position can arrive after you've shown a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MultiPV 3&lt;/code&gt;&lt;/strong&gt; gives the top three lines in one search. That powers the "compare the top lines" view, so users can see &lt;em&gt;why&lt;/em&gt; the best move beats the alternatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick vs Deep: time-boxed search
&lt;/h2&gt;

&lt;p&gt;Rather than searching to a fixed depth, the tool uses a &lt;strong&gt;fixed time per search&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quick: about 1.5 seconds.&lt;/strong&gt; Enough for most positions on most devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep: about 5 seconds.&lt;/strong&gt; For sharp tactical positions where a longer search changes the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixed depth sounds cleaner, but depth 20 takes wildly different times on a desktop and a budget phone. A time budget keeps the experience predictable, and the engine simply reaches a deeper depth on faster hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making engine output readable
&lt;/h2&gt;

&lt;p&gt;Raw UCI output isn't something most chess players want to read. The UI turns it into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An arrow on the board&lt;/strong&gt; for the best move&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An evaluation&lt;/strong&gt;: positive favours White, negative favours Black, and &lt;strong&gt;M&lt;/strong&gt; means a forced mate is on the board&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The top three lines&lt;/strong&gt;, written as normal chess moves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UCI score is always from the side to move's perspective, so you have to &lt;strong&gt;flip the sign when it's Black to move&lt;/strong&gt; to show a consistent White-relative evaluation. It's a classic first bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  FEN in, FEN out
&lt;/h2&gt;

&lt;p&gt;Most users arrive with a position from somewhere else: a game on Chess.com or Lichess, a puzzle, a book. So the tool accepts a &lt;strong&gt;FEN string&lt;/strong&gt; and also lets you set up the board by hand. Every position can also be &lt;strong&gt;shared with a link&lt;/strong&gt;, so you can send it to a friend and ask "what would you play here?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is in there
&lt;/h2&gt;

&lt;p&gt;Once the engine was running client-side, adding more tools on top of it was cheap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Game review&lt;/strong&gt; from a PGN, plus practice from your own mistakes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;720 tactics puzzles&lt;/strong&gt; from the Lichess open puzzle database, filterable by difficulty and theme&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;opening trainer&lt;/strong&gt; for 20 openings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play against the computer&lt;/strong&gt; at several levels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it reuses the same in-browser engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weaker devices give shallower analysis&lt;/strong&gt; in the same time. For a move-finder that's acceptable; for deep correspondence analysis it wouldn't be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First load downloads the engine and network.&lt;/strong&gt; The lite build keeps that reasonable, and the browser caches it afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server means no central analysis cache.&lt;/strong&gt; Every user computes their own positions. That's fine at this scale, and it's also why there's nothing to rate-limit.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you play chess (or just want to see Stockfish running in a tab), give it a go: &lt;strong&gt;&lt;a href="https://chessmovefinder.com" rel="noopener noreferrer"&gt;chessmovefinder.com&lt;/a&gt;&lt;/strong&gt;. Paste a FEN, pick who moves, and see what Stockfish thinks.&lt;/p&gt;

&lt;p&gt;I'd love feedback, especially from anyone who has shipped WebAssembly engines in production. What would you have done differently?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webassembly</category>
      <category>nextjs</category>
      <category>chess</category>
    </item>
  </channel>
</rss>
