<?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: Prashant Piprotar</title>
    <description>The latest articles on DEV Community by Prashant Piprotar (@prashplus).</description>
    <link>https://dev.to/prashplus</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%2F1511786%2F385ef3fe-57fb-4cbb-af97-608b9213f0b0.png</url>
      <title>DEV Community: Prashant Piprotar</title>
      <link>https://dev.to/prashplus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prashplus"/>
    <language>en</language>
    <item>
      <title>AetherScan: Building a Real-Time 3D Reconstruction App with Fast3R, WebSockets, and React Three Fiber</title>
      <dc:creator>Prashant Piprotar</dc:creator>
      <pubDate>Thu, 30 Jul 2026 10:50:55 +0000</pubDate>
      <link>https://dev.to/prashplus/aetherscan-building-a-real-time-3d-reconstruction-app-with-fast3r-websockets-and-react-three-2286</link>
      <guid>https://dev.to/prashplus/aetherscan-building-a-real-time-3d-reconstruction-app-with-fast3r-websockets-and-react-three-2286</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You've got a bunch of photos of an object taken from different angles. You want a 3D model. Most tools make you upload images, wait, download a file, then open it in a separate viewer.&lt;/p&gt;

&lt;p&gt;I wanted something better — &lt;strong&gt;drop your images in a browser and watch the 3D point cloud build in front of you, in real time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's &lt;a href="https://github.com/prashplus/AetherScan" rel="noopener noreferrer"&gt;AetherScan&lt;/a&gt;.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flegoelsapk91uaf35hvx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flegoelsapk91uaf35hvx.png" alt="AetherScan UI" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Drag &amp;amp; drop&lt;/strong&gt; images into the web UI&lt;/li&gt;
&lt;li&gt;Images are streamed to the backend via &lt;strong&gt;WebSocket&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast3R&lt;/strong&gt; (Facebook Research's neural 3D reconstruction model) runs GPU inference&lt;/li&gt;
&lt;li&gt;Reconstructed 3D points stream back to the browser &lt;strong&gt;in real-time&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The point cloud renders live at &lt;strong&gt;60fps&lt;/strong&gt; using React Three Fiber&lt;/li&gt;
&lt;li&gt;Export the result as a &lt;code&gt;.PLY&lt;/code&gt; file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No page reloads. No file downloads mid-process. Just upload → watch → export.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────┐          ┌───────────────────────────────┐
│       Next.js Frontend       │          │       FastAPI Backend         │
│                              │          │                              │
│  DropZone ──► FileReader     │          │  WebSocket Endpoint          │
│              │               │          │      │                       │
│         base64 chunks        │◄──WS───►│  Decode &amp;amp; Save to /tmp       │
│              │               │          │      │                       │
│  usePointStream Hook ◄───────│          │  Fast3R (PyTorch/CUDA)       │
│      │                       │          │      │                       │
│  React Three Fiber Canvas    │          │  Stream points back          │
│      │                       │          │                              │
│  Live 3D Point Cloud         │          │                              │
└──────────────────────────────┘          └───────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire pipeline is &lt;strong&gt;bidirectional over a single WebSocket&lt;/strong&gt; — no REST calls for the heavy lifting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js 15, React 19, React Three Fiber, Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI, PyTorch, Fast3R (ViT-Large), CUDA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;WebSocket (binary-friendly JSON streaming)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra&lt;/td&gt;
&lt;td&gt;Docker Compose with NVIDIA GPU passthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Interesting Engineering Bits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Streaming Points Over WebSocket
&lt;/h3&gt;

&lt;p&gt;The backend doesn't wait for the full reconstruction to finish. As soon as points are computed, they're streamed in chunks of 100:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# backend/main.py
&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# backpressure
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the user &lt;strong&gt;instant visual feedback&lt;/strong&gt; — points start appearing within seconds, even for large reconstructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Zero-Re-render Point Cloud Updates
&lt;/h3&gt;

&lt;p&gt;Normally, React re-renders the entire component tree when state changes. With 100K+ points arriving in rapid chunks, that would be a disaster.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;usePointStream&lt;/code&gt; hook bypasses React's state entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend/hooks/usePointStream.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;positionsRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Float32Array&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxPoints&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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;colorsRef&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Float32Array&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxPoints&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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;addPoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PointData&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;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;positionsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &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;point&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;points&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;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentCount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;point&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;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;point&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="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&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="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;currentCount&lt;/span&gt;&lt;span class="o"&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;// Update GPU buffer directly — no setState&lt;/span&gt;
    &lt;span class="nx"&gt;positionAttributeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;geometryRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDrawRange&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;currentCount&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;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Float32Array&lt;/code&gt; in refs&lt;/strong&gt;, not state — no re-renders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DynamicDrawUsage&lt;/code&gt;&lt;/strong&gt; tells Three.js the buffer will change often&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setDrawRange&lt;/code&gt;&lt;/strong&gt; incrementally reveals points without re-uploading the entire buffer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct &lt;code&gt;needsUpdate&lt;/code&gt;&lt;/strong&gt; flag pushes data to the GPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: &lt;strong&gt;1M+ points at 60fps&lt;/strong&gt; with zero React re-render overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fast3R Inference with Dynamic Resolution
&lt;/h3&gt;

&lt;p&gt;Consumer GPUs have limited VRAM. The backend dynamically adjusts the input resolution based on image count to prevent CUDA OOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# backend/services/reconstruction.py
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;num_images&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;img_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;num_images&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;img_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;384&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;img_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also uses mixed precision (&lt;code&gt;float16&lt;/code&gt;) on CUDA for 2x memory savings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16-mixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Eager Model Loading at Startup
&lt;/h3&gt;

&lt;p&gt;The Fast3R ViT-Large model is ~1.8 GB. Instead of lazy-loading on the first request (and making the user wait), the model loads eagerly at server startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.on_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;startup_event&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_reconstruction_service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_in_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;/status&lt;/code&gt; endpoint returns &lt;code&gt;503&lt;/code&gt; until the model is ready, so health checks and load balancers know when the service can accept traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Client-Side PLY Export
&lt;/h3&gt;

&lt;p&gt;The export happens entirely in the browser — no round-trip to the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`ply
format ascii 1.0
element vertex &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;points&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="s2"&gt;
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
end_header
`&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;blob&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;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&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 gets a standard &lt;code&gt;.PLY&lt;/code&gt; file compatible with MeshLab, Blender, CloudCompare, and other 3D tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running It Yourself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker (Recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/prashplus/AetherScan.git
&lt;span class="nb"&gt;cd &lt;/span&gt;AetherScan
docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires NVIDIA GPU + &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html" rel="noopener noreferrer"&gt;NVIDIA Container Toolkit&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend API&lt;/strong&gt;: &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swagger Docs&lt;/strong&gt;: &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Local Development
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Backend (Python 3.11 + conda)&lt;/span&gt;
conda create &lt;span class="nt"&gt;-n&lt;/span&gt; aether &lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.11
conda activate aether
&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
git clone &lt;span class="nt"&gt;--recursive&lt;/span&gt; https://github.com/facebookresearch/fast3r.git ../fast3r
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; ../fast3r
uvicorn main:app &lt;span class="nt"&gt;--reload&lt;/span&gt; &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000

&lt;span class="c"&gt;# Frontend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Apple Silicon Support
&lt;/h3&gt;

&lt;p&gt;The backend auto-detects the best available device. On a Mac with Apple Silicon, it falls back to MPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_best_device&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WebSocket &amp;gt; REST for streaming workloads.&lt;/strong&gt; Chunked HTTP responses work, but WebSockets give you true bidirectional communication — the client can send images and receive points on the same connection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bypass React for hot data paths.&lt;/strong&gt; React's reconciliation is great for UI state. For high-throughput data like point clouds, write directly to GPU buffers via refs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eager model loading pays off.&lt;/strong&gt; A 30-second startup delay is much better than a 30-second hang on the first user request. Health checks and readiness probes make this safe in production.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic resolution prevents OOM.&lt;/strong&gt; Instead of failing with a CUDA out-of-memory error, scale down input resolution based on the workload. Users get results at slightly lower fidelity rather than a crash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ship a working fallback.&lt;/strong&gt; If the ML model fails to load (no GPU, no internet for weights), the system falls back to demo points. The UI never breaks.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binary WebSocket frames&lt;/strong&gt; — switch from JSON to raw &lt;code&gt;Float32Array&lt;/code&gt; over binary frames for 5-10x bandwidth reduction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level-of-Detail (LOD)&lt;/strong&gt; — progressive point cloud rendering for very large scenes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-object support&lt;/strong&gt; — segment and reconstruct individual objects from a scene&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mesh generation&lt;/strong&gt; — go from point clouds to textured meshes using Poisson surface reconstruction&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/prashplus/AetherScan" rel="noopener noreferrer"&gt;github.com/prashplus/AetherScan&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast3R Paper&lt;/strong&gt;: &lt;a href="https://github.com/facebookresearch/fast3r" rel="noopener noreferrer"&gt;Facebook Research&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this useful, star the repo and let me know what you build with it!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>react</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>From Drone Video to 3D Maps: Automate WebODM Setup with GPU Acceleration</title>
      <dc:creator>Prashant Piprotar</dc:creator>
      <pubDate>Thu, 30 Jul 2026 10:43:40 +0000</pubDate>
      <link>https://dev.to/prashplus/from-drone-video-to-3d-maps-automate-webodm-setup-with-gpu-acceleration-1l4p</link>
      <guid>https://dev.to/prashplus/from-drone-video-to-3d-maps-automate-webodm-setup-with-gpu-acceleration-1l4p</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Drone Mapping Shouldn't Be This Hard
&lt;/h2&gt;

&lt;p&gt;You've got a drone. You've captured stunning aerial footage. Now you want to turn those videos into orthophoto maps, 3D models, or point clouds — and you hit a wall.&lt;/p&gt;

&lt;p&gt;Setting up &lt;a href="https://www.opendronemap.org/webodm/" rel="noopener noreferrer"&gt;WebODM&lt;/a&gt; (the open-source drone mapping platform built on OpenDroneMap) involves juggling Docker Compose files, configuring processing nodes, extracting frames from video, and — if you want any reasonable speed — wiring up GPU acceleration. Each step has its own pitfalls across Windows, Linux, and macOS (including Apple Silicon).&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/prashplus/WebODM-Setup" rel="noopener noreferrer"&gt;WebODM-Setup&lt;/a&gt;&lt;/strong&gt; to solve this. One repo. One command. Full pipeline from raw drone video to finished map.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Repo Does
&lt;/h2&gt;

&lt;p&gt;At its core, the toolkit automates the entire drone-mapping workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capture Video → Extract Frames → Upload to WebODM → Process → Export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what's inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-command install scripts&lt;/strong&gt; for Windows (PowerShell), Linux (Bash), and macOS (including native Apple Silicon support)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose configs&lt;/strong&gt; that wire up the full WebODM stack — webapp, worker, NodeODM processing node, PostGIS database, and Redis broker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU acceleration&lt;/strong&gt; out of the box (NVIDIA GPUs via the &lt;code&gt;nodeodm:gpu&lt;/code&gt; Docker image)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video processing scripts&lt;/strong&gt; — extract frames at configurable FPS with GPS metadata preservation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing&lt;/strong&gt; for multi-video workflows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Start (Under 5 Minutes)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker Desktop (Windows/macOS) or Docker Engine (Linux)&lt;/li&gt;
&lt;li&gt;Python 3.8+&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Linux / macOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/prashplus/WebODM-Setup.git
&lt;span class="nb"&gt;cd &lt;/span&gt;WebODM-Setup
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x scripts/&lt;span class="k"&gt;*&lt;/span&gt;.sh
./scripts/install-linux.sh
./scripts/start-webodm.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Windows (PowerShell)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/prashplus/WebODM-Setup.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WebODM-Setup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\scripts\install-windows.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\scripts\start-webodm.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8000&lt;/code&gt; — done.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Apple Silicon users&lt;/strong&gt;: The scripts auto-detect M1/M2/M3/M4 and use the correct arm64-native, CPU-only Docker images. No extra config needed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; spins up five services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;webapp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opendronemap/webodm_webapp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Web UI + API (port 8000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;worker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opendronemap/webodm_webapp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Celery task worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;node-odm-1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opendronemap/nodeodm:gpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Photogrammetry engine (port 3000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;db&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgis/postgis:14-3.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostGIS database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;broker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redis:7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Task queue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code&gt;node-register&lt;/code&gt; init container auto-registers the processing node with the webapp on first boot — no manual setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  GPU Acceleration: The Speed Multiplier
&lt;/h2&gt;

&lt;p&gt;This is where things get exciting. By default, the compose file uses the &lt;code&gt;nodeodm:gpu&lt;/code&gt; image with NVIDIA GPU reservation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;node-odm-1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendronemap/nodeodm:gpu&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GPU_ENABLED=true&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia&lt;/span&gt;
            &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-World Performance (200 images, 20MP)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Processing Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU only (8 cores)&lt;/td&gt;
&lt;td&gt;~2–4 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU (RTX 4060, 8GB VRAM)&lt;/td&gt;
&lt;td&gt;~30–60 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a &lt;strong&gt;2–5x speedup&lt;/strong&gt; across the photogrammetry pipeline — Structure from Motion, Multi-View Stereo, meshing, and texturing all benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;NVIDIA GPU with CUDA 11.0+ and 4GB+ VRAM&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/NVIDIA/nvidia-docker" rel="noopener noreferrer"&gt;NVIDIA Container Toolkit&lt;/a&gt; installed&lt;/li&gt;
&lt;li&gt;On Windows: WSL2 backend with GPU passthrough enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't have an NVIDIA GPU, just swap the image to &lt;code&gt;opendronemap/nodeodm:latest&lt;/code&gt; and remove the &lt;code&gt;deploy&lt;/code&gt; section — everything works on CPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  From Video to Map: The Processing Pipeline
&lt;/h2&gt;

&lt;p&gt;Most consumer drones record video, not individual photos. The repo includes Python scripts to bridge that gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract Frames
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1 frame per second, JPEG quality 95&lt;/span&gt;
python scripts/extract-frames.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; DJI_0001.MP4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; ./frames/project1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fps&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quality&lt;/span&gt; 95

&lt;span class="c"&gt;# With GPS metadata extraction&lt;/span&gt;
python scripts/extract-frames.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; DJI_0001.MP4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; ./frames/project1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fps&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extract-gps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Batch Process Multiple Videos
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/batch-process.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input-dir&lt;/span&gt; ./videos &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-dir&lt;/span&gt; ./frames &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fps&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Choosing the Right FPS
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flight Speed&lt;/th&gt;
&lt;th&gt;Recommended FPS&lt;/th&gt;
&lt;th&gt;~Frames per 5-min Video&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slow (2–3 m/s)&lt;/td&gt;
&lt;td&gt;0.5–1&lt;/td&gt;
&lt;td&gt;150–300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal (4–5 m/s)&lt;/td&gt;
&lt;td&gt;1–2&lt;/td&gt;
&lt;td&gt;300–600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast (6+ m/s)&lt;/td&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;td&gt;600–900&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key is &lt;strong&gt;overlap&lt;/strong&gt; — consecutive frames need 70–80% overlap for the photogrammetry algorithms to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Processing Presets
&lt;/h2&gt;

&lt;p&gt;Once frames are uploaded to WebODM, choose a preset based on your needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Time (200 imgs, 8C/16GB)&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fast&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;td&gt;Quick previews, data validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1.5 hours&lt;/td&gt;
&lt;td&gt;Most projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4 hours&lt;/td&gt;
&lt;td&gt;Professional deliverables, detailed 3D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ultra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8+ hours&lt;/td&gt;
&lt;td&gt;Maximum detail, heritage documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Output Formats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orthophoto&lt;/strong&gt; (GeoTIFF) — georeferenced 2D map&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D Model&lt;/strong&gt; (OBJ/PLY) — textured mesh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point Cloud&lt;/strong&gt; (PLY/LAS) — colored XYZ data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEM&lt;/strong&gt; (GeoTIFF) — elevation/terrain model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contours&lt;/strong&gt; (GeoJSON) — elevation contour lines&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;p&gt;The workflow documentation covers four common scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agriculture Mapping&lt;/strong&gt; — Grid pattern at 100m, orthophoto export for NDVI analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construction Site Surveys&lt;/strong&gt; — DEM + orthophoto for volume calculations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building/Monument 3D Models&lt;/strong&gt; — Circular flight pattern, high-quality 3D mesh export&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure Inspection&lt;/strong&gt; — Multi-angle capture, detailed textured model&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebODM-Setup/
├── scripts/
│   ├── install-windows.ps1 / install-linux.sh
│   ├── start-webodm.ps1 / start-webodm.sh
│   ├── stop-webodm.ps1 / stop-webodm.sh
│   ├── update-webodm.ps1 / update-webodm.sh
│   ├── extract-frames.py
│   ├── batch-process.py
│   └── common.sh
├── config/
│   ├── webodm-config.env
│   └── processing-presets.json
├── docker-compose.yml                # GPU-enabled (Linux/Windows)
├── docker-compose.apple-silicon.yml  # Apple Silicon (arm64, CPU-only)
├── requirements.txt
├── GPU_SETUP.md
├── MACOS_APPLE_SILICON.md
├── WORKFLOW.md
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tips &amp;amp; Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker memory&lt;/strong&gt;: Bump to 8GB+ in Docker Desktop settings. Processing large datasets with the default 2GB will crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port conflicts&lt;/strong&gt;: If port 8000 is taken, edit &lt;code&gt;config/webodm-config.env&lt;/code&gt; and set &lt;code&gt;WO_PORT=8080&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU monitoring&lt;/strong&gt;: Run &lt;code&gt;watch -n 2 nvidia-smi&lt;/code&gt; (Linux) or a looping &lt;code&gt;nvidia-smi&lt;/code&gt; (Windows PowerShell) to monitor utilization during processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg&lt;/strong&gt;: Required for the frame extraction scripts. Install via &lt;code&gt;brew install ffmpeg&lt;/code&gt;, &lt;code&gt;apt install ffmpeg&lt;/code&gt;, or &lt;code&gt;choco install ffmpeg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image quality matters&lt;/strong&gt;: Blurry, overexposed, or sky-only frames will degrade results. Curate your frame set before uploading.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;The project is MIT-licensed and welcomes contributions. Fork, branch, PR — the standard flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/your-feature
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add your feature"&lt;/span&gt;
git push origin feature/your-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/prashplus/WebODM-Setup" rel="noopener noreferrer"&gt;github.com/prashplus/WebODM-Setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebODM Docs&lt;/strong&gt;: &lt;a href="https://docs.webodm.org/" rel="noopener noreferrer"&gt;docs.webodm.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenDroneMap Community&lt;/strong&gt;: &lt;a href="https://community.opendronemap.org/" rel="noopener noreferrer"&gt;community.opendronemap.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU Setup Guide&lt;/strong&gt;: &lt;a href="https://github.com/prashplus/WebODM-Setup/blob/main/GPU_SETUP.md" rel="noopener noreferrer"&gt;GPU_SETUP.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Silicon Guide&lt;/strong&gt;: &lt;a href="https://github.com/prashplus/WebODM-Setup/blob/main/MACOS_APPLE_SILICON.md" rel="noopener noreferrer"&gt;MACOS_APPLE_SILICON.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If this saves you time, &lt;a href="https://github.com/prashplus/WebODM-Setup" rel="noopener noreferrer"&gt;drop a ⭐ on the repo&lt;/a&gt; — it helps others find it. Questions or issues? Open a GitHub issue or reach out in the comments below.&lt;/p&gt;

&lt;p&gt;Happy mapping! 🗺️&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>docker</category>
      <category>drones</category>
      <category>gis</category>
    </item>
    <item>
      <title>I Built a Cross-Platform Browser Benchmark That Stress Tests Any Device — Here's How</title>
      <dc:creator>Prashant Piprotar</dc:creator>
      <pubDate>Thu, 30 Jul 2026 10:26:02 +0000</pubDate>
      <link>https://dev.to/prashplus/i-built-a-cross-platform-browser-benchmark-that-stress-tests-any-device-heres-how-ln</link>
      <guid>https://dev.to/prashplus/i-built-a-cross-platform-browser-benchmark-that-stress-tests-any-device-heres-how-ln</guid>
      <description>&lt;h2&gt;
  
  
  No installs. No native apps. Just open a URL and push your MacBook, PC, Android, or iPhone to its limits.
&lt;/h2&gt;




&lt;p&gt;We've all been there. You get a shiny new device — a MacBook Pro with an M4 chip, a Galaxy S25, or a gaming PC with the latest GPU — and the first thing you want to do is &lt;em&gt;see what it can really do&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The problem? Most benchmark tools are platform-locked native apps. Geekbench costs money. 3DMark is Windows-only. AnTuTu is mobile-only. And none of them let you compare your MacBook against your phone with the same test suite.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://www.prashplus.com/web-stress-test/" rel="noopener noreferrer"&gt;WebStress&lt;/a&gt;&lt;/strong&gt; — an open-source, browser-based benchmark suite that runs on literally anything with a modern browser. No installs, no accounts, no app store. Just open a URL.&lt;/p&gt;

&lt;p&gt;Here's what I learned building it, and how you can use (or fork) it yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a Browser-Based Benchmark?
&lt;/h2&gt;

&lt;p&gt;Browsers have become absurdly powerful. WebGL gives us GPU access. Web Workers enable multi-threaded CPU tests. TypedArrays let us do real number-crunching. The browser is no longer just a document viewer — it's a compute platform.&lt;/p&gt;

&lt;p&gt;A browser benchmark has one killer advantage: &lt;strong&gt;true cross-platform parity&lt;/strong&gt;. The exact same JavaScript code runs on your M4 MacBook, your Windows gaming rig, your Android phone, and your iPhone. Same tests, same scoring, genuinely comparable results.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Tests
&lt;/h2&gt;

&lt;p&gt;WebStress runs &lt;strong&gt;32 individual sub-tests&lt;/strong&gt; across 6 categories. Let me break down what each one actually measures and why it matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 CPU — The Brain
&lt;/h3&gt;

&lt;p&gt;The CPU benchmark isn't just "loop a billion times and see how fast it goes." It targets specific computational patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integer Arithmetic&lt;/strong&gt; — Bitwise operations, XOR chains, and integer multiplication. This is your chip's bread and butter for general-purpose computing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Floating Point&lt;/strong&gt; — Trigonometric functions (&lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;cos&lt;/code&gt;, &lt;code&gt;atan2&lt;/code&gt;), square roots, logarithms. Critical for scientific computing, audio processing, and physics simulations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash / Crypto&lt;/strong&gt; — FNV-1a hashing over 1KB buffers. Simulates real-world hashing workloads like integrity checks and password processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Array Sort&lt;/strong&gt; — Sorting 10,000-element Float64 arrays. Tests branch prediction, cache efficiency, and the JS engine's sort implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prime Sieve&lt;/strong&gt; — Classic trial division up to 500K. Pure integer ALU throughput with branching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix Multiply&lt;/strong&gt; — 64×64 double-precision matrix multiplication. The fundamental operation behind machine learning, graphics, and scientific computing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Core Scaling&lt;/strong&gt; — Spawns Web Workers equal to your core count, all running the integer test simultaneously. Measures how well your chip scales under parallel load and how efficiently the browser manages threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The multi-core test is particularly interesting. Apple's M-series chips show very different scaling behavior from Intel/AMD due to their efficiency/performance core architecture. You can literally watch it in the results.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎮 GPU — The Graphics Muscle
&lt;/h3&gt;

&lt;p&gt;These tests use WebGL shaders — actual GPU programs running on your graphics hardware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fill Rate&lt;/strong&gt; — How many full-screen quads the GPU can paint per second. Raw pixel throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragment Shader&lt;/strong&gt; — A complex procedural shader with trigonometric wave functions. Tests the GPU's shader unit compute performance — the same units that process every pixel in games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geometry Throughput&lt;/strong&gt; — Renders 5,000 animated triangles. Measures vertex processing and how fast the GPU can handle geometry transformations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texture Sampling&lt;/strong&gt; — Multi-level texture reads with coordinate distortion. Tests texture unit throughput and memory bandwidth — critical for game rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Particle System&lt;/strong&gt; — 10,000 point sprites with alpha blending. Combines vertex processing, fragment shading, and blending — a realistic GPU workload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integrated GPUs (Intel, Apple, Qualcomm Adreno) versus discrete GPUs (NVIDIA, AMD) show dramatic differences here. Even between phones, the GPU gap can be 5-10x.&lt;/p&gt;

&lt;h3&gt;
  
  
  💾 Memory — The Hidden Bottleneck
&lt;/h3&gt;

&lt;p&gt;CPU and GPU get all the headlines, but memory performance often determines real-world speed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sequential Read/Write&lt;/strong&gt; — 32MB linear scan. Tests main memory bandwidth when access patterns are predictable — the best case scenario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random Access&lt;/strong&gt; — 8MB random reads. This is the worst case. It exposes memory latency and how well the cache hierarchy handles unpredictable access patterns. The gap between sequential and random performance reveals your cache effectiveness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth&lt;/strong&gt; — Tests copy speed across buffer sizes from 8KB to 32MB. This maps directly to cache levels — L1, L2, L3, and main memory. You can literally see the performance cliff as data exceeds each cache level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocation Speed&lt;/strong&gt; — How fast the runtime can allocate and initialize memory buffers. Important for applications that create lots of temporary objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypedArray Operations&lt;/strong&gt; — Vectorized multiply-add across 8M floats. Tests how well the JS engine optimizes tight numeric loops — some engines auto-vectorize these to SIMD instructions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🤖 AI / NPU — The New Frontier
&lt;/h3&gt;

&lt;p&gt;Every chip maker is adding AI accelerators. These tests simulate common ML workloads in pure JavaScript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix Multiply (GEMM)&lt;/strong&gt; — 256×256 float32 matrix multiplication. This is THE operation behind neural networks. Measures raw FLOPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conv2D&lt;/strong&gt; — 3×3 convolution with 16 output channels over a 128×128 image with ReLU activation. The core operation in image recognition models like ResNet and YOLO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Softmax&lt;/strong&gt; — 10,000-element softmax across 1,000 iterations. Used in every attention layer and classifier output. Tests exp/log throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attention Score&lt;/strong&gt; — Full scaled dot-product attention with Q, K, V matrices (64×64). This is the mechanism inside every transformer model — GPT, BERT, Llama, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mini-Network Inference&lt;/strong&gt; — A 3-layer feedforward network (784→256→128→10) processing batches of 32. Simulates real model inference — like running a digit classifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently these run in pure JS on the CPU. As WebNN browser support matures, these same workloads could be dispatched to dedicated NPU hardware (Apple Neural Engine, Qualcomm Hexagon, Intel NPU). The scores will become a direct measure of on-device AI capability.&lt;/p&gt;

&lt;h3&gt;
  
  
  🖼️ Rendering — Browser Performance
&lt;/h3&gt;

&lt;p&gt;Your device might have a fast CPU, but can the browser &lt;em&gt;use&lt;/em&gt; it efficiently?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DOM Manipulation&lt;/strong&gt; — Creates 1,000 styled elements, reads layout properties, applies transforms, then removes them. This is what happens in every complex web app — React, Vue, Angular all do this constantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canvas 2D&lt;/strong&gt; — Renders 200 circles, 100 lines, and 50 text labels per frame for 3 seconds. Tests the browser's 2D rendering pipeline — used in charts, games, and data visualization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS Animations&lt;/strong&gt; — Animates 500 elements simultaneously with rotation and translation. Tests the browser's compositor and animation scheduler — critical for smooth UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout / Reflow&lt;/strong&gt; — Rebuilds a 50-item flexbox grid 500 times, forcing layout recalculation each time. This is the most expensive thing browsers do, and the main source of UI jank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG Rendering&lt;/strong&gt; — Creates and renders 200 SVG documents with circles and Bézier curves. Tests the vector rendering pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📦 Storage — I/O Speed
&lt;/h3&gt;

&lt;p&gt;How fast can the browser persist and retrieve data?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IndexedDB Write/Read&lt;/strong&gt; — Individual transaction read/write operations. The primary storage API for web apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IndexedDB Bulk&lt;/strong&gt; — 5,000 records in a single transaction with cursor-based reading. Tests transactional throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LocalStorage&lt;/strong&gt; — Synchronous key-value storage. Fast but blocking — these tests reveal just how blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache API&lt;/strong&gt; — Write and read from the browser's Cache Storage. Used by service workers and PWAs for offline support.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Live Monitoring
&lt;/h2&gt;

&lt;p&gt;One thing I wanted that other benchmarks don't do well: &lt;strong&gt;watching the test happen in real time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While benchmarks run, you see four live-updating charts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FPS&lt;/strong&gt; — Browser frame rate, showing when the main thread is saturated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; — Rolling score updates as each sub-test completes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thermal&lt;/strong&gt; — A stability metric that detects CPU throttling. It runs a quick workload repeatedly and measures timing consistency. When your chip heats up and throttles, the variance increases. It's indirect, but it actually works — you can watch a MacBook go from "Cool" to "Warm" during the GPU tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — JS heap usage in real-time (Chrome/Edge only, via &lt;code&gt;performance.memory&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Scoring System
&lt;/h2&gt;

&lt;p&gt;Every sub-test produces a raw metric (operations/second, FPS, GB/s, etc.). These get normalized to a 0–5000 scale against baseline values.&lt;/p&gt;

&lt;p&gt;The baselines are calibrated so that a mid-range 2024 device scores roughly 1000. A high-end device scores 2000–3000+. The maximum is 5000, leaving room for future hardware.&lt;/p&gt;

&lt;p&gt;The overall score is the mean across all category scores, giving equal weight to CPU, GPU, Memory, AI, Rendering, and Storage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results &amp;amp; Analytics
&lt;/h2&gt;

&lt;p&gt;After tests complete, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Animated score ring&lt;/strong&gt; with your overall score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category cards&lt;/strong&gt; with bar charts and per-sub-test breakdowns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radar chart&lt;/strong&gt; showing your device's strengths and weaknesses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeline chart&lt;/strong&gt; showing score progression across tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full details table&lt;/strong&gt; with every raw measurement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; — JSON download or one-click clipboard copy for sharing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Technical Decisions
&lt;/h2&gt;

&lt;p&gt;A few choices I made and why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pure vanilla JS, zero dependencies.&lt;/strong&gt; No React, no build step, no npm. The entire project is static HTML/CSS/JS files. This means it works from a &lt;code&gt;file://&lt;/code&gt; URL, from any static host, and loads instantly. For a benchmark tool, framework overhead would pollute the measurements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline Web Workers.&lt;/strong&gt; Workers normally need a separate JS file loaded via URL, which fails on &lt;code&gt;file://&lt;/code&gt; due to security restrictions. I inline the worker code as a string and create a Blob URL. This means the multi-core CPU test works everywhere — even opening &lt;code&gt;index.html&lt;/code&gt; directly from disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebGL, not WebGPU.&lt;/strong&gt; WebGPU is the future, but WebGL2 has 95%+ browser support including mobile Safari. For a tool that needs to work &lt;em&gt;everywhere&lt;/em&gt;, WebGL is the right choice today. WebGPU variants can be added as an optional enhanced test later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canvas-based charts, not Chart.js/D3.&lt;/strong&gt; Again, zero dependencies. The mini-line charts, radar chart, and timeline are drawn directly to &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. They're simple, fast, and add zero to the bundle size.&lt;/p&gt;




&lt;h2&gt;
  
  
  Some Interesting Findings
&lt;/h2&gt;

&lt;p&gt;Testing this across devices revealed patterns you don't see in spec sheets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple Silicon is absurdly efficient.&lt;/strong&gt; An M2 MacBook Air outperforms many desktop CPUs on single-thread while barely getting warm. The multi-core scaling efficiency is also excellent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile GPUs are surprisingly good.&lt;/strong&gt; A recent iPhone's GPU fragment shader performance isn't that far behind integrated laptop GPUs. The Adreno and Mali GPUs in flagship Android phones are legitimate compute devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage performance varies wildly.&lt;/strong&gt; IndexedDB on Safari is significantly slower than Chrome. LocalStorage blocking time can be 10x worse on some browsers. If your web app feels slow, storage might be why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thermal throttling is real and visible.&lt;/strong&gt; Run the full suite on a fanless device and watch the thermal stability metric drop. Phones throttle aggressively after 30-60 seconds of sustained load. The benchmark captures this behavior that synthetic spec sheets ignore.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The project is fully open source. You can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run it now&lt;/strong&gt; — Head to &lt;a href="https://www.prashplus.com/web-stress-test/" rel="noopener noreferrer"&gt;&lt;strong&gt;prashplus.com/web-stress-test&lt;/strong&gt;&lt;/a&gt; and hit "Run Benchmark"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone it&lt;/strong&gt; — Fork the &lt;a href="https://github.com/prashplus/web-stress-test" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; and run locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend it&lt;/strong&gt; — Add your own tests, adjust baselines, build a leaderboard
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/prashplus/web-stress-test.git
&lt;span class="nb"&gt;cd &lt;/span&gt;web-stress-test
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000
&lt;span class="c"&gt;# Open http://localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No build step. No &lt;code&gt;npm install&lt;/code&gt;. Just code and a browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Some ideas for future versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebGPU compute shaders&lt;/strong&gt; for true GPU compute benchmarks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebNN integration&lt;/strong&gt; to actually test NPU hardware when browser support lands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WASM benchmarks&lt;/strong&gt; comparing JS vs WebAssembly performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network speed tests&lt;/strong&gt; measuring latency and throughput&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery impact&lt;/strong&gt; estimation for mobile devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboard&lt;/strong&gt; with anonymous result submission for comparison&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical tracking&lt;/strong&gt; to see how browser updates affect performance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Your browser is a more powerful computing platform than most people realize. WebStress proves it by pushing every subsystem — CPU, GPU, memory, AI, rendering, and storage — through real workloads, on any device, with zero friction.&lt;/p&gt;

&lt;p&gt;The web platform is the only truly universal runtime. Building a benchmark on it means anyone, anywhere, on any device can test and compare their hardware with a single click.&lt;/p&gt;

&lt;p&gt;Give it a try, share your scores, and let me know what tests you'd want to see added.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;WebStress is open source under the MIT license. &lt;a href="https://www.prashplus.com/web-stress-test/" rel="noopener noreferrer"&gt;Try it live&lt;/a&gt; or star it on &lt;a href="https://github.com/prashplus/web-stress-test" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you find it useful.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: JavaScript, WebGL, Benchmarking, Performance, Web Development, Open Source, Browser, Cross-Platform&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>performance</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
