<?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: jack</title>
    <description>The latest articles on DEV Community by jack (@jackwind).</description>
    <link>https://dev.to/jackwind</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%2F3652799%2F758f595c-6af2-496c-b1a5-9d700b3d24c9.jpg</url>
      <title>DEV Community: jack</title>
      <link>https://dev.to/jackwind</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jackwind"/>
    <language>en</language>
    <item>
      <title>What the Browser Gamepad API Can—and Cannot—Tell You About Controller Health</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/jackwind/what-the-browser-gamepad-api-can-and-cannot-tell-you-about-controller-health-51ph</link>
      <guid>https://dev.to/jackwind/what-the-browser-gamepad-api-can-and-cannot-tell-you-about-controller-health-51ph</guid>
      <description>&lt;p&gt;A controller can appear to work normally while still having problems that affect gameplay.&lt;/p&gt;

&lt;p&gt;A stick may not return perfectly to the center. A trigger may fail to reach its full range. Wireless mode may produce less stable updates than a wired connection. A controller may also behave differently across browsers, operating systems, and connection modes.&lt;/p&gt;

&lt;p&gt;Most online controller testers only visualize buttons and axes. That is useful, but it does not answer a more important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is the controller actually healthy?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While building &lt;a href="https://controllertestonline.com/" rel="noopener noreferrer"&gt;Controller Tester Online&lt;/a&gt;, I explored how far the browser Gamepad API can go beyond basic input visualization.&lt;/p&gt;

&lt;p&gt;The result is a browser-based testing workflow that checks buttons, triggers, analog sticks, stick drift, circularity, deadzone behavior, update stability, and vibration support.&lt;/p&gt;

&lt;p&gt;However, browser-based testing also has important limitations. In particular, browser measurements should not be confused with low-level USB hardware analysis.&lt;/p&gt;

&lt;p&gt;This article explains what the Gamepad API can measure reliably, what it can only estimate, and how I approached building a more complete controller health check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading a Controller in the Browser
&lt;/h2&gt;

&lt;p&gt;The Gamepad API provides access to controllers connected to a user's device.&lt;/p&gt;

&lt;p&gt;A basic implementation can start like this:&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;gamepads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getGamepads&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;gamepad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepads&lt;/span&gt;&lt;span class="p"&gt;[&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;gamepad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&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;The browser exposes several useful properties:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timestamp&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each button, the browser can usually provide:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;touched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Digital buttons normally return values close to either &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Analog inputs, such as triggers, may return values across a range.&lt;/p&gt;

&lt;p&gt;Analog sticks are usually represented as axes between approximately &lt;code&gt;-1&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A typical layout might look like this:&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;leftStickX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;leftStickY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rightStickX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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;rightStickY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact axis and button mapping can vary depending on the controller, driver, browser, operating system, and whether the controller uses the standard mapping.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the User Usually Has to Press a Button First
&lt;/h2&gt;

&lt;p&gt;Browsers do not always expose a connected controller immediately.&lt;/p&gt;

&lt;p&gt;In many cases, the user must first press a button or move a stick. This behavior helps prevent websites from silently identifying connected gaming devices without user interaction.&lt;/p&gt;

&lt;p&gt;A controller testing page therefore needs a clear initial state, such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Connect your controller, then press any button to activate it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The page can also listen for connection events:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gamepadconnected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Controller connected:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gamepaddisconnected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Controller disconnected:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;However, connection events alone are not enough. The application still needs to read the latest controller state continuously.&lt;/p&gt;

&lt;p&gt;A common approach is to use &lt;code&gt;requestAnimationFrame&lt;/code&gt;:&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;function&lt;/span&gt; &lt;span class="nf"&gt;updateController&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="nx"&gt;gamepads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getGamepads&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;gamepad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepads&lt;/span&gt;&lt;span class="p"&gt;[&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;gamepad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateButtons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;updateAxes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateController&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateController&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well for visualization, but it is important to understand that the rendering loop is not necessarily the same as the controller's hardware polling loop.&lt;/p&gt;

&lt;p&gt;That distinction becomes important later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Raw Input Is Not a Health Check
&lt;/h2&gt;

&lt;p&gt;Showing live button and stick values is useful, but it does not automatically tell the user whether the controller has a problem.&lt;/p&gt;

&lt;p&gt;For example, a stick at rest might report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X: 0.021
Y: -0.034
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does that mean the stick is drifting?&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;Small center deviations are common. Their impact depends on several factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The size of the deviation&lt;/li&gt;
&lt;li&gt;Whether the value is stable or constantly moving&lt;/li&gt;
&lt;li&gt;The deadzone used by the game&lt;/li&gt;
&lt;li&gt;The controller model&lt;/li&gt;
&lt;li&gt;The connection mode&lt;/li&gt;
&lt;li&gt;Calibration and driver behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A better controller test therefore needs timed sampling rather than relying on a single reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring Stick Drift
&lt;/h2&gt;

&lt;p&gt;For a basic drift test, the user should place the controller on a stable surface and avoid touching the analog sticks.&lt;/p&gt;

&lt;p&gt;The application can then sample both sticks for several seconds.&lt;/p&gt;

&lt;p&gt;A simplified implementation might look like this:&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;function&lt;/span&gt; &lt;span class="nf"&gt;getStickMagnitude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sampleStick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamepad&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="nx"&gt;leftX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&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;leftY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&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="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&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;rightX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&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;rightY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;leftX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;leftY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getStickMagnitude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leftX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leftY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rightX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rightY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getStickMagnitude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rightX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rightY&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the test, the tool can record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average X and Y values&lt;/li&gt;
&lt;li&gt;Maximum distance from the center&lt;/li&gt;
&lt;li&gt;Variation over time&lt;/li&gt;
&lt;li&gt;The percentage of samples outside a chosen threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&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;let&lt;/span&gt; &lt;span class="nx"&gt;maximumMagnitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;magnitudeSum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sampleCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addSample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&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="nx"&gt;magnitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;maximumMagnitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maximumMagnitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;magnitudeSum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;sampleCount&lt;/span&gt; &lt;span class="o"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAverageMagnitude&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="nx"&gt;sampleCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;magnitudeSum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sampleCount&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 single short spike should not necessarily produce a severe warning. It is usually more useful to consider both the maximum deviation and the average behavior during the test.&lt;/p&gt;

&lt;p&gt;The result can then be presented as practical guidance rather than an absolute diagnosis.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Left stick maximum center deviation: 3.2%
Suggested initial deadzone: approximately 4%–5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suggested deadzone should be slightly higher than the observed center deviation, while still remaining low enough to preserve responsiveness.&lt;/p&gt;

&lt;p&gt;It is also important to avoid claiming that one threshold is correct for every controller.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Stick Range and Circularity
&lt;/h2&gt;

&lt;p&gt;A healthy analog stick should normally reach close to its expected outer range when rotated around the edge.&lt;/p&gt;

&lt;p&gt;To test this, the user can rotate the stick slowly through a complete circle.&lt;/p&gt;

&lt;p&gt;Each sample can be plotted on a two-dimensional coordinate system:&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;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A perfectly circular result would follow an ideal unit circle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x² + y² = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real controllers rarely produce a perfect circle.&lt;/p&gt;

&lt;p&gt;Some may produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slightly square output&lt;/li&gt;
&lt;li&gt;Restricted diagonal movement&lt;/li&gt;
&lt;li&gt;Uneven maximum range&lt;/li&gt;
&lt;li&gt;Different results in different directions&lt;/li&gt;
&lt;li&gt;Values that exceed the expected unit circle&lt;/li&gt;
&lt;li&gt;Incomplete outer-edge coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distance from the center can be calculated using:&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;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible to inspect whether the stick consistently reaches the outer range.&lt;/p&gt;

&lt;p&gt;However, circularity should not be treated as a simple pass-or-fail score.&lt;/p&gt;

&lt;p&gt;A controller can have imperfect circularity and still perform normally in games. Different controller firmware and calibration systems may intentionally shape analog output differently.&lt;/p&gt;

&lt;p&gt;The most useful result is usually a visual trace combined with several measurements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum outer radius&lt;/li&gt;
&lt;li&gt;Maximum outer radius&lt;/li&gt;
&lt;li&gt;Average radius&lt;/li&gt;
&lt;li&gt;Directional consistency&lt;/li&gt;
&lt;li&gt;Percentage of expected outer-edge coverage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Checking Trigger Range
&lt;/h2&gt;

&lt;p&gt;Triggers can also develop problems.&lt;/p&gt;

&lt;p&gt;A trigger may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fail to return completely to zero&lt;/li&gt;
&lt;li&gt;Fail to reach its maximum value&lt;/li&gt;
&lt;li&gt;Move inconsistently&lt;/li&gt;
&lt;li&gt;Produce unstable values&lt;/li&gt;
&lt;li&gt;Have a large inactive area at the beginning of travel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A trigger test should record both its resting value and maximum value.&lt;/p&gt;

&lt;p&gt;For example:&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;let&lt;/span&gt; &lt;span class="nx"&gt;minimumValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;maximumValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;minimumValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minimumValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;maximumValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maximumValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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;The user can then be asked to release and fully press each trigger.&lt;/p&gt;

&lt;p&gt;A result might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Left trigger resting value: 0.000
Left trigger maximum value: 0.984
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more informative than only showing a moving progress bar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Estimating Update Rate in a Browser
&lt;/h2&gt;

&lt;p&gt;Polling rate is one of the most easily misunderstood measurements in browser-based controller testing.&lt;/p&gt;

&lt;p&gt;A website does not have the same low-level access as a USB protocol analyzer or native hardware testing application.&lt;/p&gt;

&lt;p&gt;The browser may expose a &lt;code&gt;timestamp&lt;/code&gt; for the gamepad state:&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;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By observing changes in this timestamp, an application can estimate how frequently the browser receives new controller state.&lt;/p&gt;

&lt;p&gt;A basic calculation might look like this:&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;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTimestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;previousTimestamp&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;interval&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;estimatedRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;interval&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;For example, an interval of approximately 4 milliseconds would correspond to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1000 / 4 = 250 Hz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this should be described as a browser-observed estimate.&lt;/p&gt;

&lt;p&gt;Several layers can influence the result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Controller hardware&lt;/li&gt;
&lt;li&gt;USB, Bluetooth, or wireless receiver&lt;/li&gt;
&lt;li&gt;Device firmware&lt;/li&gt;
&lt;li&gt;Operating-system input stack&lt;/li&gt;
&lt;li&gt;Driver behavior&lt;/li&gt;
&lt;li&gt;Browser implementation&lt;/li&gt;
&lt;li&gt;Page visibility&lt;/li&gt;
&lt;li&gt;Rendering performance&lt;/li&gt;
&lt;li&gt;JavaScript scheduling&lt;/li&gt;
&lt;li&gt;Power-saving features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser may also reuse state between frames or expose timestamps differently across platforms.&lt;/p&gt;

&lt;p&gt;Therefore, a browser tool should not claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your controller's exact hardware polling rate is 500 Hz.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A more accurate description is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The browser observed controller updates at an estimated average rate of approximately 500 Hz during this sample.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That wording matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Polling Rate Versus Rendering Rate
&lt;/h2&gt;

&lt;p&gt;It is also important not to calculate polling rate only from &lt;code&gt;requestAnimationFrame&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most displays run at 60 Hz, 120 Hz, 144 Hz, or another fixed refresh rate. If controller state is checked only once per animation frame, the measurement may be limited by the page's rendering frequency.&lt;/p&gt;

&lt;p&gt;For example, a 60 Hz rendering loop runs approximately once every:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1000 / 60 = 16.67 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A controller may update several times within that interval, but the JavaScript code may only observe the latest state.&lt;/p&gt;

&lt;p&gt;This means:&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="nf"&gt;requestAnimationFrame&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="c1"&gt;// This loop frequency is not automatically the controller polling rate.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For browser testing, update-rate measurements are most useful for relative comparisons.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wired mode versus Bluetooth mode&lt;/li&gt;
&lt;li&gt;Bluetooth mode versus a 2.4 GHz receiver&lt;/li&gt;
&lt;li&gt;One USB port versus another&lt;/li&gt;
&lt;li&gt;Stable updates versus large irregular gaps&lt;/li&gt;
&lt;li&gt;Different browsers on the same computer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result should be interpreted as a practical browser-level signal rather than laboratory-grade hardware data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring Jitter
&lt;/h2&gt;

&lt;p&gt;Average update rate does not tell the whole story.&lt;/p&gt;

&lt;p&gt;Two controllers can have the same average estimated rate while producing very different timing consistency.&lt;/p&gt;

&lt;p&gt;Suppose one controller produces intervals like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4.0 ms
4.1 ms
3.9 ms
4.0 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another might produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.0 ms
8.0 ms
2.0 ms
5.0 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sets can produce a similar average, but the second is much less consistent.&lt;/p&gt;

&lt;p&gt;A useful test should therefore record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average interval&lt;/li&gt;
&lt;li&gt;Minimum interval&lt;/li&gt;
&lt;li&gt;Maximum interval&lt;/li&gt;
&lt;li&gt;Standard deviation&lt;/li&gt;
&lt;li&gt;Large timing spikes&lt;/li&gt;
&lt;li&gt;Distribution of intervals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified standard-deviation calculation might look like this:&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;function&lt;/span&gt; &lt;span class="nf"&gt;standardDeviation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;values&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;===&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;return&lt;/span&gt; &lt;span class="mi"&gt;0&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;average&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;values&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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="nx"&gt;difference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;average&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;difference&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;difference&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;values&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variance&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;This measurement can help identify unstable browser-observed updates, although it still cannot reveal exactly where in the hardware-to-browser pipeline the instability originated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detecting Vibration Support
&lt;/h2&gt;

&lt;p&gt;Some browsers and controllers expose a vibration actuator through the Gamepad API.&lt;/p&gt;

&lt;p&gt;A capability check may look like this:&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;actuator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vibrationActuator&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;actuator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vibration actuator detected&lt;/span&gt;&lt;span class="dl"&gt;"&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 short vibration test might use:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gamepad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vibrationActuator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;playEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dual-rumble&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;strongMagnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;weakMagnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;However, the availability of the API does not guarantee that vibration will work in every environment.&lt;/p&gt;

&lt;p&gt;Support may depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser&lt;/li&gt;
&lt;li&gt;The operating system&lt;/li&gt;
&lt;li&gt;The controller&lt;/li&gt;
&lt;li&gt;The connection method&lt;/li&gt;
&lt;li&gt;Browser permissions&lt;/li&gt;
&lt;li&gt;Driver support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this reason, a browser tool should distinguish between:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A vibration interface being exposed&lt;/li&gt;
&lt;li&gt;A vibration command being accepted&lt;/li&gt;
&lt;li&gt;The user confirming that physical vibration occurred&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Capability detection alone is not always enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping Controller Tests Local
&lt;/h2&gt;

&lt;p&gt;A controller testing tool does not need to upload input data to a server.&lt;/p&gt;

&lt;p&gt;Button states, axes, timestamps, drift samples, and test results can all be processed locally in JavaScript.&lt;/p&gt;

&lt;p&gt;This provides several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Immediate feedback&lt;/li&gt;
&lt;li&gt;No account requirement&lt;/li&gt;
&lt;li&gt;Lower latency&lt;/li&gt;
&lt;li&gt;Better privacy&lt;/li&gt;
&lt;li&gt;Easier testing across devices&lt;/li&gt;
&lt;li&gt;No need to store controller identifiers or input history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The report can also be generated locally from the completed tests.&lt;/p&gt;

&lt;p&gt;For a tool like this, browser-side processing is not just convenient. It is usually the most appropriate architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a Complete Testing Workflow
&lt;/h2&gt;

&lt;p&gt;Instead of presenting every metric at once, I organized the testing process into a sequence.&lt;/p&gt;

&lt;p&gt;A typical workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect and activate the controller&lt;/li&gt;
&lt;li&gt;Check the detected controller name and mapping&lt;/li&gt;
&lt;li&gt;Test every button&lt;/li&gt;
&lt;li&gt;Test both triggers&lt;/li&gt;
&lt;li&gt;Move both sticks through their full range&lt;/li&gt;
&lt;li&gt;Leave the sticks untouched for a drift test&lt;/li&gt;
&lt;li&gt;Rotate each stick for a circularity test&lt;/li&gt;
&lt;li&gt;Sample browser-observed update intervals&lt;/li&gt;
&lt;li&gt;Check vibration support&lt;/li&gt;
&lt;li&gt;Generate a summary report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This makes the process easier for non-technical users while still exposing detailed measurements for developers and advanced users.&lt;/p&gt;

&lt;p&gt;The live implementation is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://controllertestonline.com/" rel="noopener noreferrer"&gt;Try Controller Tester Online&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tests run inside the browser, and controller input data is processed locally.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Browser Testing Can Tell You
&lt;/h2&gt;

&lt;p&gt;A browser-based controller test can be useful for identifying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buttons that do not register&lt;/li&gt;
&lt;li&gt;Buttons that remain active after release&lt;/li&gt;
&lt;li&gt;Triggers with incomplete travel&lt;/li&gt;
&lt;li&gt;Analog sticks that do not return near the center&lt;/li&gt;
&lt;li&gt;Significant stick drift&lt;/li&gt;
&lt;li&gt;Restricted stick range&lt;/li&gt;
&lt;li&gt;Uneven directional output&lt;/li&gt;
&lt;li&gt;Irregular browser-observed update timing&lt;/li&gt;
&lt;li&gt;Differences between connection modes&lt;/li&gt;
&lt;li&gt;Whether vibration support is available to the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests are also useful when buying or selling a used controller, comparing connection modes, troubleshooting input issues, or checking a controller before starting a game.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Browser Testing Cannot Prove
&lt;/h2&gt;

&lt;p&gt;A browser test cannot always determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The exact USB hardware polling rate&lt;/li&gt;
&lt;li&gt;The precise source of input latency&lt;/li&gt;
&lt;li&gt;Whether a problem comes from firmware, drivers, the browser, or the operating system&lt;/li&gt;
&lt;li&gt;How every game applies its own deadzone and response curve&lt;/li&gt;
&lt;li&gt;Whether a controller meets laboratory calibration standards&lt;/li&gt;
&lt;li&gt;Whether a wireless timing spike came from interference or software scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser measurements should therefore be treated as diagnostic evidence, not absolute hardware certification.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons From Building the Tool
&lt;/h2&gt;

&lt;p&gt;The main lesson was that visualization is easy, but interpretation is difficult.&lt;/p&gt;

&lt;p&gt;Displaying an axis value is straightforward.&lt;/p&gt;

&lt;p&gt;Explaining whether that value indicates a meaningful problem requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple samples&lt;/li&gt;
&lt;li&gt;Clear testing instructions&lt;/li&gt;
&lt;li&gt;Appropriate thresholds&lt;/li&gt;
&lt;li&gt;Transparent limitations&lt;/li&gt;
&lt;li&gt;Results that avoid false precision&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same applies to polling rate.&lt;/p&gt;

&lt;p&gt;A number such as &lt;code&gt;250 Hz&lt;/code&gt; appears precise, but the surrounding measurement method matters more than the number itself.&lt;/p&gt;

&lt;p&gt;A trustworthy testing tool should clearly separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct values reported by the Gamepad API&lt;/li&gt;
&lt;li&gt;Calculations based on timed samples&lt;/li&gt;
&lt;li&gt;Browser-level estimates&lt;/li&gt;
&lt;li&gt;User-confirmed hardware behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction helps users understand what the result actually means.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feedback From Other Developers
&lt;/h2&gt;

&lt;p&gt;Controller behavior varies significantly across devices and platforms, so broader testing is useful.&lt;/p&gt;

&lt;p&gt;I would be interested to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which controllers produce unusual button or axis mappings?&lt;/li&gt;
&lt;li&gt;Do you see different results in Chrome, Edge, Firefox, or Safari?&lt;/li&gt;
&lt;li&gt;How different are your wired, Bluetooth, and 2.4 GHz results?&lt;/li&gt;
&lt;li&gt;Does vibration work correctly on your operating system?&lt;/li&gt;
&lt;li&gt;Are there additional controller health metrics that would be useful?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can test your controller at &lt;a href="https://controllertestonline.com/" rel="noopener noreferrer"&gt;Controller Tester Online&lt;/a&gt; and share your browser, operating system, controller model, and connection method in the comments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; AI was used to help improve the English wording and article structure. The testing methodology, implementation details, and technical conclusions were reviewed by the author.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Why I Split My Mouse Tester Into Its Own Site — And What "Low 5%" Actually Tells You About Your Mouse</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:22:36 +0000</pubDate>
      <link>https://dev.to/jackwind/why-i-split-my-mouse-tester-into-its-own-site-and-what-low-5-actually-tells-you-about-your-7i7</link>
      <guid>https://dev.to/jackwind/why-i-split-my-mouse-tester-into-its-own-site-and-what-low-5-actually-tells-you-about-your-7i7</guid>
      <description>&lt;h1&gt;
  
  
  Why I Split My Mouse Tester Into Its Own Site — And What "Low 5%" Actually Tells You About Your Mouse
&lt;/h1&gt;

&lt;p&gt;After my last post about bypassing Chrome's event loop to measure polling rates, I got a question I couldn't stop thinking about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I have a 4000Hz Razer mouse. The test shows an average of 3,847Hz. Is that... good? Or is something wrong?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The honest answer: &lt;strong&gt;the average doesn't tell you.&lt;/strong&gt; And that gap between "what the spec says" and "what actually matters in-game" is exactly why I ended up building &lt;a href="https://mousepollingratetest.com" rel="noopener noreferrer"&gt;mousepollingratetest.com&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A quick backstory
&lt;/h2&gt;

&lt;p&gt;My original hardware testing project lives at &lt;a href="https://hardwaretest.org" rel="noopener noreferrer"&gt;hardwaretest.org&lt;/a&gt; — a broader tool covering multiple peripherals. Mouse polling rate was just one feature among many there.&lt;/p&gt;

&lt;p&gt;But after publishing those first posts, mouse polling rate questions kept coming: from competitive FPS players, from mechanical keyboard folks curious about crossover behavior, from people debugging USB dropouts. The topic was pulling more search traffic and user engagement than any other test on the site.&lt;/p&gt;

&lt;p&gt;So I split it out. One domain, one job. And in the process of rebuilding it as a standalone tool, I had to confront a question I'd been sidestepping: &lt;strong&gt;what metrics actually matter?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with average polling rate
&lt;/h2&gt;

&lt;p&gt;Most browser-based polling rate testers show you one number: your average Hz. That number feels satisfying — you see "998 Hz" and think "close enough to 1000, we're good."&lt;/p&gt;

&lt;p&gt;But imagine two mice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mouse A&lt;/strong&gt;: Every single report arrives at exactly 1ms intervals. Average: 1000Hz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mouse B&lt;/strong&gt;: Most reports arrive at 1ms, but every 20th report takes 3ms. Average: ~952Hz.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a benchmark, Mouse B looks fine. In a fast-paced FPS, that 3ms dropout is the exact moment your crosshair skips. Your aim didn't fail — your hardware did.&lt;/p&gt;

&lt;p&gt;This is why I built three additional metrics into mousepollingratetest.com that I think tell a more complete story.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three metrics that actually matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Low 5% (the "worst-case" rate)
&lt;/h3&gt;

&lt;p&gt;Borrowed from storage benchmarking (where it's called P5 latency), &lt;strong&gt;Low 5%&lt;/strong&gt; filters out your best 95% of samples and shows you the average of the slowest 5%.&lt;/p&gt;

&lt;p&gt;In plain terms: it's what your mouse does when things go wrong. USB bus contention, CPU spikes, background processes — they all show up here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example from a real test session:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mouse&lt;/th&gt;
&lt;th&gt;Average Hz&lt;/th&gt;
&lt;th&gt;Low 5% Hz&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mouse A (1000Hz mode)&lt;/td&gt;
&lt;td&gt;987&lt;/td&gt;
&lt;td&gt;961&lt;/td&gt;
&lt;td&gt;-26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mouse B (1000Hz mode)&lt;/td&gt;
&lt;td&gt;994&lt;/td&gt;
&lt;td&gt;743&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-251&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mouse B looks better on average. Mouse B is the worse gaming mouse. The Low 5% exposed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Jitter (interval standard deviation)
&lt;/h3&gt;

&lt;p&gt;Jitter measures how &lt;em&gt;consistent&lt;/em&gt; your polling intervals are, expressed as the standard deviation of the time between events (in milliseconds).&lt;/p&gt;

&lt;p&gt;A 1000Hz mouse should fire every 1.0ms. If your jitter is 0.08ms, your intervals are ranging roughly from 0.92ms to 1.08ms — tight, stable, good. If your jitter is 0.4ms, your mouse is swinging between 0.6ms and 1.4ms intervals — you'll feel that as cursor microstutter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stable mouse:  [1.01, 0.99, 1.00, 1.02, 0.98] → jitter: 0.015ms ✅
Jittery mouse: [0.72, 1.31, 0.88, 1.45, 0.64] → jitter: 0.38ms  ⚠️
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jitter under ~0.1ms is excellent. Above 0.3ms, you're likely to notice it in precision tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Stability Score (and the Grade)
&lt;/h3&gt;

&lt;p&gt;Stability is a composite percentage that combines sample count, jitter, and dropout rate into a single signal. The Grade (S / A / B / C) maps this to something easier to act on at a glance.&lt;/p&gt;

&lt;p&gt;An "S" grade means your hardware + USB path + OS are all playing nicely together. A "C" means &lt;em&gt;something&lt;/em&gt; is wrong — but the individual metrics above tell you what.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Chrome Extension angle
&lt;/h2&gt;

&lt;p&gt;One thing I hadn't anticipated: a lot of people want to check polling rate without navigating to a website mid-game. So I shipped a companion Chrome Extension — toolbar popup, zero permissions, 6-second test, same metrics.&lt;/p&gt;

&lt;p&gt;It routes back to mousepollingratetest.com for the full histogram and Leaderboard, but the quick check is fully self-contained.&lt;/p&gt;

&lt;p&gt;Zero-permission browser extensions are a nice constraint to build against: no storage, no network, everything computed and discarded in memory. The implementation is simpler than the main site in some ways, more limited in others (no IndexedDB for local result history, for example).&lt;/p&gt;




&lt;h2&gt;
  
  
  What 8000Hz actually looks like in a browser
&lt;/h2&gt;

&lt;p&gt;For the 4000Hz/8000Hz crowd: browser tests will always underreport at extreme polling rates. Here's why.&lt;/p&gt;

&lt;p&gt;When your mouse fires at 8000Hz, it's producing a report every 0.125ms. Chrome's &lt;code&gt;pointermove&lt;/code&gt; event handler is bound by the browser's internal event processing loop — which, even with &lt;code&gt;setPointerCapture&lt;/code&gt; and raw input hints, can't reliably resolve sub-0.5ms intervals without coalescing.&lt;/p&gt;

&lt;p&gt;So if mousepollingratetest.com shows you 3,800Hz on an 8000Hz mouse, that's not a bug. That's the browser's effective delivery rate after OS scheduling and USB interrupt handling. For real-world comparison purposes (how does my mouse perform &lt;em&gt;in Chrome&lt;/em&gt;?), that's actually the right number to care about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Want the raw hardware number? You need a native app. Want to know how your mouse behaves in every browser-based game you play? You're already in the right place.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I'd build next
&lt;/h2&gt;

&lt;p&gt;If you've gotten this far, you might as well go test your mouse: &lt;strong&gt;&lt;a href="https://mousepollingratetest.com" rel="noopener noreferrer"&gt;mousepollingratetest.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Leaderboard is where things get interesting — seeing how different mice cluster by brand, polling mode, and USB configuration. A 2000Hz Logitech G Pro X Superlight 2 on a dedicated USB controller vs. a USB hub tells a very different stability story.&lt;/p&gt;

&lt;p&gt;If you have a high-Hz mouse, I'd genuinely like to see your Low 5% numbers. Drop them in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previously in this series:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/jackwind/i-tried-vibe-coding-a-hardware-test-site-ai-is-powerful-but-its-not-magic-yet-4gn5"&gt;I Tried "Vibe Coding" a Hardware Test Site: AI is Powerful, But It's Not Magic (Yet)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/jackwind/vibe-coded-a-hardware-tester-in-a-weekend-spoiler-the-ui-took-10-mins-the-physics-took-2-days-1f3j"&gt;Vibe Coded a Hardware Tester in a Weekend. (Spoiler: The UI took 10 mins, the Physics took 2 days)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/jackwind/is-1000hz-possible-in-chrome-bypassing-the-event-loop-to-test-mouse-polling-rate-3b9k"&gt;Is 1000Hz possible in Chrome? Bypassing the Event Loop to test Mouse Polling Rate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Is 1000Hz possible in Chrome? Bypassing the Event Loop to test Mouse Polling Rate</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Thu, 25 Dec 2025 02:01:48 +0000</pubDate>
      <link>https://dev.to/jackwind/is-1000hz-possible-in-chrome-bypassing-the-event-loop-to-test-mouse-polling-rate-3b9k</link>
      <guid>https://dev.to/jackwind/is-1000hz-possible-in-chrome-bypassing-the-event-loop-to-test-mouse-polling-rate-3b9k</guid>
      <description>&lt;p&gt;As a PC gamer, I've always hated installing 500MB of bloatware (looking at you, Razer Synapse and G-Hub) just to verify if my mouse is actually running at its advertised 1000Hz polling rate.&lt;/p&gt;

&lt;p&gt;I asked myself: Why can't I just test this in the browser?&lt;/p&gt;

&lt;p&gt;The common answer is: "You can't. The browser is too slow. requestAnimationFrame is capped at your monitor's refresh rate (usually 60Hz or 144Hz)."&lt;/p&gt;

&lt;p&gt;Challenge accepted. 🛠️&lt;/p&gt;

&lt;p&gt;I spent the last few weeks building &lt;a href="https://hardwaretest.org/mouse-polling-rate-test/" rel="noopener noreferrer"&gt;https://hardwaretest.org/mouse-polling-rate-test/&lt;/a&gt;, a privacy-first diagnostic suite. The hardest part was getting accurate, raw input readings that bypass the visual rendering loop.&lt;/p&gt;

&lt;p&gt;Here is how I solved the "1000Hz Problem" in Vanilla JS.&lt;/p&gt;

&lt;p&gt;The Problem: Visuals vs. Inputs&lt;br&gt;
Most web animations rely on requestAnimationFrame (rAF). If your monitor is 60Hz, rAF fires every ~16.6ms. If you try to measure a 1000Hz mouse (which sends data every 1ms) inside a rAF loop, you will miss 94% of the data.&lt;/p&gt;

&lt;p&gt;We need to decouple the Input Thread from the Render Thread.&lt;/p&gt;

&lt;p&gt;The Solution: pointermove + performance.now()&lt;br&gt;
We can't rely on the screen painting. We need to listen to the raw pointermove event, which fires much more frequently than the screen updates in modern browsers (Chrome/Edge handle this effectively via "Coalesced Events" logic, though we want the raw stream).&lt;/p&gt;

&lt;p&gt;But standard Date.now() isn't precise enough. It gives us milliseconds. For 1000Hz, we need microseconds.&lt;/p&gt;

&lt;p&gt;Enter the High Resolution Time API.&lt;/p&gt;

&lt;p&gt;The Code&lt;br&gt;
Here is the simplified logic of how I calculate the instant Polling Rate (Hz):&lt;/p&gt;

&lt;p&gt;JavaScript&lt;/p&gt;

&lt;p&gt;let lastEventTime = 0;&lt;br&gt;
let pollingRateSamples = [];&lt;/p&gt;

&lt;p&gt;window.addEventListener('pointermove', (event) =&amp;gt; {&lt;br&gt;
  // 1. Get the microsecond-precise timestamp&lt;br&gt;
  const currentEventTime = performance.now();&lt;/p&gt;

&lt;p&gt;// 2. Calculate the delta (time between two events)&lt;br&gt;
  const timeDelta = currentEventTime - lastEventTime;&lt;/p&gt;

&lt;p&gt;// 3. Convert to Frequency (Hz)&lt;br&gt;
  // Hz = 1000ms / delta&lt;br&gt;
  const instantaneousHz = 1000 / timeDelta;&lt;/p&gt;

&lt;p&gt;// 4. Filter out noise (idle or super slow movements)&lt;br&gt;
  if (instantaneousHz &amp;gt; 10 &amp;amp;&amp;amp; instantaneousHz &amp;lt; 8000) {&lt;br&gt;
    pollingRateSamples.push(instantaneousHz);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;lastEventTime = currentEventTime;&lt;/p&gt;

&lt;p&gt;// NOTE: Do NOT update the DOM here!&lt;br&gt;
  // Updating the UI every 1ms will kill the browser performance.&lt;br&gt;
  // Store the data, and visualize it in a separate requestAnimationFrame loop.&lt;br&gt;
});&lt;br&gt;
The "Event Loop" Trap&lt;br&gt;
The tricky part is that browsers try to "group" events to save battery.&lt;/p&gt;

&lt;p&gt;If you run a heavy operation inside that pointermove listener, the browser will start throttling the event firing rate to match the frame rate.&lt;/p&gt;

&lt;p&gt;The trick is:&lt;/p&gt;

&lt;p&gt;Keep the event listener extremely lightweight (just math and array pushing).&lt;/p&gt;

&lt;p&gt;De-couple the visualization. Use a separate requestAnimationFrame loop to read from the pollingRateSamples array and draw the graph.&lt;/p&gt;

&lt;p&gt;The Result&lt;br&gt;
By separating the input capture from the rendering, I managed to get consistent readings matching local software:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vaknzle5r48v76cua7n.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vaknzle5r48v76cua7n.gif" alt=" " width="8" height="14"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can try the live demo here: Mouse Polling Rate Test&lt;/p&gt;

&lt;p&gt;It also includes a Gamepad Tester (for stick drift) and a Dead Pixel test, all running locally in the browser.&lt;/p&gt;

&lt;p&gt;Discussion 💬&lt;br&gt;
I built this to prove that modern Web APIs are powerful enough to replace most "utility" desktop software.&lt;/p&gt;

&lt;p&gt;What do you think? Are we at a point where we can finally uninstall those 500MB driver suites, or will native apps always be king for hardware interaction?&lt;/p&gt;

&lt;p&gt;Let me know in the comments! 👇&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vibe Coded a Hardware Tester in a Weekend. (Spoiler: The UI took 10 mins, the Physics took 2 days)</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Wed, 17 Dec 2025 04:15:46 +0000</pubDate>
      <link>https://dev.to/jackwind/vibe-coded-a-hardware-tester-in-a-weekend-spoiler-the-ui-took-10-mins-the-physics-took-2-days-1f3j</link>
      <guid>https://dev.to/jackwind/vibe-coded-a-hardware-tester-in-a-weekend-spoiler-the-ui-took-10-mins-the-physics-took-2-days-1f3j</guid>
      <description>&lt;p&gt;The Vibe: I wanted to build a simple, privacy-focused tool to test PC hardware (Mice, Keyboards, Screens) without installing shady .exe files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F42h0ogckaohf9n8pbxdl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F42h0ogckaohf9n8pbxdl.png" alt=" " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Tool: I opened my AI editor (Cursor/Claude), entered "flow state," and started prompting. My goal was &lt;a href="http://www.hardwaretest.org" rel="noopener noreferrer"&gt;www.hardwaretest.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is what I learned about the reality of Vibe Coding a tool that requires high-precision hardware interaction.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where Vibe Coding Shines (The UI)
This part felt like magic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Make it dark mode." -&amp;gt; Done.&lt;/p&gt;

&lt;p&gt;"Give me a grid of clickable colors for dead pixel testing." -&amp;gt; Done.&lt;/p&gt;

&lt;p&gt;"Make it responsive for mobile." -&amp;gt; Done.&lt;/p&gt;

&lt;p&gt;In about 2 hours, I had a beautiful, functional frontend deployed on Vercel. If I were hand-coding the CSS/Tailwind, this would have taken me a full day.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where the Vibe Died (The Logic)
Then I tried to implement the Keyboard Polling Rate Test. I needed to detect if a gaming keyboard was actually reporting at 1000Hz (1ms latency).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I asked the AI: "Write a script to measure key press intervals." The AI confidently spit out code using standard Date.now() and addEventListener.&lt;/p&gt;

&lt;p&gt;The Result? Jittery garbage data. The AI didn't understand that:&lt;/p&gt;

&lt;p&gt;The Browser Event Loop is often slower than a high-end keyboard.&lt;/p&gt;

&lt;p&gt;Date.now() isn't precise enough for sub-millisecond measurements.&lt;/p&gt;

&lt;p&gt;DOM updates (rendering the number) cause layout thrashing that slows down the measurement.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "Manual Mode" Fix
I had to snap out of "Vibe Mode" and put on my Senior Engineer hat. I spent the next 48 hours manually debugging and rewriting the core logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Switched to performance.now() for microsecond precision.&lt;/p&gt;

&lt;p&gt;Implemented a sliding window average algorithm to smooth out the browser's main thread jitter.&lt;/p&gt;

&lt;p&gt;Decoupled the Input Loop (data capture) from the Render Loop (requestAnimationFrame).&lt;/p&gt;

&lt;p&gt;The Verdict&lt;br&gt;
Vibe Coding is a force multiplier, but it's not a replacement for domain knowledge.&lt;/p&gt;

&lt;p&gt;AI built the vessel (UI, Layout, SEO schema).&lt;/p&gt;

&lt;p&gt;I had to build the engine (Timing algorithms, Physics).&lt;/p&gt;

&lt;p&gt;The tool is now live and stable. It features a Keyboard Tester, Mouse Double-Click Detector, and a Dead Pixel Fixer (Canvas-based).&lt;/p&gt;

&lt;p&gt;Check it out here: 👉 &lt;a href="http://www.hardwaretest.org" rel="noopener noreferrer"&gt;www.hardwaretest.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm curious—has anyone else hit a wall where AI implies "it works" but the underlying physics/logic is completely broken?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Tried "Vibe Coding" a Hardware Test Site: AI is Powerful, But It's Not Magic (Yet) 🛠️</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Tue, 09 Dec 2025 01:35:32 +0000</pubDate>
      <link>https://dev.to/jackwind/i-tried-vibe-coding-a-hardware-test-site-ai-is-powerful-but-its-not-magic-yet-4gn5</link>
      <guid>https://dev.to/jackwind/i-tried-vibe-coding-a-hardware-test-site-ai-is-powerful-but-its-not-magic-yet-4gn5</guid>
      <description>&lt;p&gt;👋 The Backstory&lt;br&gt;
I want to share a recent "experimental project" of mine: HardwareTest.org.&lt;/p&gt;

&lt;p&gt;The motivation was simple: I bought some new peripherals and wanted to test them. But I was fed up with the existing tools—screens full of ads, outdated UIs, or sketchy .exe files that I didn't want to download.&lt;/p&gt;

&lt;p&gt;As a self-described "average developer," I recently got brainwashed by the concept of "Vibe Coding" (coding by natural language/AI intuition). I thought, "AI is so strong now. I'll just write the prompts, let the AI write the code, and I'll be done in minutes, right?"&lt;/p&gt;

&lt;p&gt;Spoiler Alert: I was too naive. 😂&lt;br&gt;
While AI absolutely lowered the barrier to entry and boosted my speed by 10x, taking a tool from "it works" to "it feels good to use" was full of hidden traps.&lt;/p&gt;

&lt;p&gt;🚧 The Real Challenges&lt;br&gt;
Here is a breakdown of the actual struggles I faced while pair-programming with AI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Tooling Chaos&lt;br&gt;
My workflow was a bit of a mess. I started with Antigravity (it designed the initial UI), but ran out of credits. I switched to Codex to finish the logic. For the blog content, I used Gemini, but integrating that content back into the project via Codex resulted in a formatting nightmare. It was a lot of back-and-forth "fixing" what the AI broke.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Browser Limitations vs. Physics (The Keyboard Test)&lt;br&gt;
I thought testing Keyboard Polling Rate would be simple: just tell the AI to "write an event listener."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Reality: I discovered that the browser's Event Loop often can't even keep up with a 1000Hz gaming keyboard. The raw data coming out was jittery and unusable. The Fix: I was forced into dozens of rounds of conversation with the AI. We had to optimize the algorithm, add debounce logic, and implement sliding averages just to get a relatively accurate "Real-time Hz Dashboard" on the web.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Devil is in the Details (The Mouse Test)&lt;br&gt;
I assumed a mouse test was just listening for onClick. The Reality: To properly test for Double Click issues (a gamer's nightmare) and Scroll Wheel rollback, you need very precise counting logic. Also, the AI kept confusing "Middle Click" (pressing the wheel) with "Scrolling" (spinning the wheel). It took a lot of human intervention to separate those events cleanly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The SEO Battle&lt;br&gt;
Writing the code was just step one. To get this English-language site indexed by Google, I spent ages wrestling with Schema, FAQ, and JSON-LD. The Insight: AI writes syntactically correct code, but often logically nonsensical SEO tags. This led to Google Search Console errors that I had to manually debug and patch.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;✨ The Result&lt;/p&gt;

&lt;p&gt;Despite the process being more twisted than I expected, I’m still proud of what this experiment produced.&lt;br&gt;
It became a pure static, ad-free, dark-mode online hardware diagnostic suite — and more importantly, a real-world test of what AI-assisted coding can and cannot do well.&lt;/p&gt;

&lt;p&gt;The original experiment is available here:&lt;br&gt;
👉 &lt;a href="https://hardwaretest.org" rel="noopener noreferrer"&gt;https://hardwaretest.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Current Features:&lt;/p&gt;

&lt;p&gt;⌨️ Keyboard Test: Visualizer with a real-time Hz polling rate dashboard (and Ghosting/NKRO support).&lt;/p&gt;

&lt;p&gt;🖱️ Mouse Test: Left/Right/Middle buttons + Scroll Wheel + Double Click detection.&lt;/p&gt;

&lt;p&gt;🖥️ Dead Pixel &amp;amp; Fixer: Standard color cycle test, plus a "High-Frequency Noise Repair" feature built with Canvas.&lt;/p&gt;

&lt;p&gt;🎧 Audio Test: Left/Right channel separation + Logarithmic Sweep.&lt;/p&gt;

&lt;p&gt;This "Vibe Coding" experience taught me a valuable lesson: AI is an incredibly fast junior developer. It can speed up production by 1000%, but it cannot yet replace the human eye for product details, edge cases, and user experience.&lt;/p&gt;

&lt;p&gt;🙏 Feedback Welcome! The site just went live, so there are definitely bugs and rough edges. If you have a moment to try it out, I’d love to hear your feedback in the comments!&lt;/p&gt;




&lt;h2&gt;
  
  
  Update (Several weeks later)
&lt;/h2&gt;

&lt;p&gt;After the initial launch, I ran into a problem I didn’t expect at all — SEO.&lt;/p&gt;

&lt;p&gt;The AI-generated structure worked fine visually, but Google treated most pages as duplicates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;very similar raw HTML&lt;/li&gt;
&lt;li&gt;reused meta descriptions&lt;/li&gt;
&lt;li&gt;canonical pointing to the homepage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, many URLs were canonicalized away in Google Search Console.&lt;/p&gt;

&lt;p&gt;Instead of patching the old structure endlessly, I decided to take a more radical approach:&lt;br&gt;
I extracted the most requested feature — mouse polling rate testing — and rebuilt it as &lt;strong&gt;&lt;a href="https://mousepollingratetest.com/" rel="noopener noreferrer"&gt;a dedicated mouse polling rate testing tool&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This time, I avoided “AI-generated SEO” entirely and relied on proper SEO fundamentals.&lt;br&gt;
The difference in indexing and visibility was immediately noticeable.&lt;/p&gt;

&lt;p&gt;This experience reinforced something important for me:&lt;br&gt;
AI can write code fast, but search engines still reward human judgment and structure.&lt;/p&gt;

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