<?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>Forem: Hopp</title>
    <description>The latest articles on Forem by Hopp (@gethopp).</description>
    <link>https://forem.com/gethopp</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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11152%2Fc3919057-9ffe-41c9-bdc5-95483adebf8c.png</url>
      <title>Forem: Hopp</title>
      <link>https://forem.com/gethopp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gethopp"/>
    <language>en</language>
    <item>
      <title>Sentry Errors Are Unreadable by Default. Here's How to Fix It with Source Maps</title>
      <dc:creator>Iason Paraskevopoulos</dc:creator>
      <pubDate>Tue, 14 Oct 2025 11:06:00 +0000</pubDate>
      <link>https://forem.com/gethopp/sentry-errors-are-unreadable-by-default-heres-how-to-fix-it-with-source-maps-435n</link>
      <guid>https://forem.com/gethopp/sentry-errors-are-unreadable-by-default-heres-how-to-fix-it-with-source-maps-435n</guid>
      <description>&lt;p&gt;At &lt;a href="https://gethopp.app/" rel="noopener noreferrer"&gt;Hopp&lt;/a&gt;, we’re building a cross-platform remote pair-programming application using Tauri and React. Like any production app, Hopp can encounter JavaScript errors in the front-end.&lt;/p&gt;

&lt;p&gt;We use Sentry for error tracking, but we initially ran into a frustrating issue: the error stack traces captured from our Tauri app were practically gibberish due to code minification and transpilation. In this post, I’ll explain how we solved this by leveraging source maps with Sentry, turning cryptic errors into human-readable ones.&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%2Fdlh49gjxx49i3.cloudfront.net%2Fsentry-sourcemaps%2FBefore.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%2Fdlh49gjxx49i3.cloudfront.net%2Fsentry-sourcemaps%2FBefore.png" alt="Sentry error stacktrace with source map applied, showing original source code lines" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is The Actual Problem
&lt;/h2&gt;

&lt;p&gt;By default, our production JavaScript is bundled and minified (compressed and optimized). When an error occurs, Sentry would report it with a stack trace referencing the minified code (often a single line of obfuscated code). This made debugging nearly impossible, as we couldn’t directly see what part of our source code caused the issue.&lt;/p&gt;

&lt;p&gt;In the sections below, I’ll walk through why this problem occurs and how using source maps (and Sentry’s build integration) can fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Error Stack Traces Break in Production
&lt;/h2&gt;

&lt;p&gt;When we build our Tauri app’s front-end, Vite produces optimized JavaScript. This involves two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;b&gt;Transpilation&lt;/b&gt;: Converting modern JSX/TypeScript into plain JavaScript that browsers (or the WebView in Tauri) can run.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;Minification&lt;/b&gt;: Removing whitespace and shortening variable names to reduce file size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are great for performance, but terrible for readability of errors. Let’s look at a quick example. Suppose our React component source looks like this (in TypeScript/JSX):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&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;HelloWorld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wow&lt;/span&gt;&lt;span class="dl"&gt;"&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the transpilation process, the transpiled output might resemble:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jsx&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;_jsx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react/jsx-runtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&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;HelloWorld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_jsx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&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;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&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;Even without full minification, you can see the code has been transformed (e.g. our JSX turned into a _jsx function call). In a fully minified bundle, this could be squeezed into one line, with variable names like &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. Now imagine an error occurs in this component – the stack trace will refer to something like &lt;code&gt;HelloWorld.js:1:12345&lt;/code&gt;, pointing to the minified code. Debugging from that is extremely difficult.&lt;/p&gt;

&lt;p&gt;We attempted a build with no minification, which made the code slightly more readable and only increased our bundle from &lt;code&gt;~12 MB&lt;/code&gt; to &lt;code&gt;~14 MB&lt;/code&gt;. However, the code was still transpiled (not identical to our source), so line numbers and function names didn’t match exactly. The real solution was to utilize source maps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Source Maps (and Why We Need Them)
&lt;/h2&gt;

&lt;p&gt;Source maps are files that map the transpiled code back to the original source code.&lt;/p&gt;

&lt;p&gt;To understand better how they work, there is an amazing website, &lt;a href="https://sokra.github.io/source-map-visualization/#custom-choose" rel="noopener noreferrer"&gt;source-map-explorer&lt;/a&gt;, where you can play around with your source maps or we the pre-built examples.&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%2Fdlh49gjxx49i3.cloudfront.net%2Fsentry-sourcemaps%2FSourceMaps.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%2Fdlh49gjxx49i3.cloudfront.net%2Fsentry-sourcemaps%2FSourceMaps.png" alt="Example source map from sokra source-map-explorer" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A source map allows debugging tools to translate from the location in a minified file to the corresponding location in your original, untransformed code. For example, if an error occurs at column &lt;code&gt;5000&lt;/code&gt; of &lt;code&gt;bundle.js&lt;/code&gt;, a source map can tell us it corresponds to, say, line &lt;code&gt;42&lt;/code&gt; of &lt;code&gt;HelloWorld.tsx&lt;/code&gt; in our source.&lt;/p&gt;

&lt;p&gt;In practice, source maps are usually &lt;code&gt;.map&lt;/code&gt; files generated alongside your JS bundles. They contain a JSON mapping of every symbol and line from the minified file to the original file. When enabled, tools like browser DevTools or Sentry can use them to display stack traces and code in terms of your original source.&lt;/p&gt;

&lt;p&gt;And then the benefit of using source maps with Sentry is that error report can show us the actual line of our React component that failed, instead of an unreadable blob of minified code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Ship Source Maps in the App?
&lt;/h2&gt;

&lt;p&gt;It might sound tempting to just include the source maps with our application bundle. Then Sentry (or even end-users in DevTools) would automatically get the decoded errors. However, there are two major drawbacks to embedding source maps in a production app:&lt;br&gt;
    1.  Huge file size increase: Source map files can be massive. In our case, the production bundle jumped from &lt;code&gt;~12 MB (minified)&lt;/code&gt; to &lt;code&gt;~47 MB&lt;/code&gt; when we included source maps! 🔼 This 4x size bloat is unacceptable to ship to every user.&lt;br&gt; For comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build Type&lt;/th&gt;
&lt;th&gt;File Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Without minification&lt;/td&gt;
&lt;td&gt;~14 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With minification&lt;/td&gt;
&lt;td&gt;~12 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With source maps included&lt;/td&gt;
&lt;td&gt;~47 MB 😱&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2.  Potentially exposing source code: Source maps essentially contain your original source. If you include them publicly, savvy users could reconstruct a lot of your source code. This might be fine for open-source projects, but not ideal for a proprietary app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;So, directly shipping source maps inside the app was a non-starter. We needed a way to get the benefits of source maps without burdening our users with large binary files. This is where Sentry’s integration comes in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Uploading Source Maps to Sentry (Using the Sentry Vite Plugin)
&lt;/h2&gt;

&lt;p&gt;Fortunately, Sentry provides tools to handle source maps on their side. The idea is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate source maps during your build (but do not bundle them with the app distributed to users).&lt;/li&gt;
&lt;li&gt;Upload those source maps to Sentry’s server.&lt;/li&gt;
&lt;li&gt;When an error occurs, Sentry will use the uploaded source maps to de-minify the stack trace, showing you the original code context in the Sentry dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, end-users never download the &lt;code&gt;.map&lt;/code&gt; files, only Sentry has them. We get the best of both worlds: &lt;strong&gt;small app size&lt;/strong&gt; and &lt;strong&gt;meaningful error debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sentry has official plugins for popular build tools (Webpack, Vite, etc.) to streamline this. In our case (using Vite), we leveraged the &lt;code&gt;@sentry/vite-plugin&lt;/code&gt;. This plugin automates the release setup and source map upload as part of the build process.&lt;/p&gt;

&lt;p&gt;Here’s how we integrated it into our Tauri app’s build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install Sentry SDK and Vite plugin: (e.g. &lt;code&gt;npm install @sentry/react @sentry/vite-plugin&lt;/code&gt;). We also set up Sentry in our app code with Sentry.init(...) including our DSN, as usual.&lt;/li&gt;
&lt;li&gt;Configure Vite to produce source maps: In &lt;code&gt;vite.config.ts&lt;/code&gt;, ensure the build is configured with sourcemap: true for production. Vite doesn’t output source maps for production builds unless you enable this.&lt;/li&gt;
&lt;li&gt;Add the Sentry Vite plugin in vite.config.ts: We include the plugin with our Sentry project details. For example:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sentryVitePlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sentry/vite-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="c1"&gt;// Enable Sentry plugin only if SENTRY_AUTH_TOKEN is present&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_AUTH_TOKEN&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
      &lt;span class="nf"&gt;sentryVitePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hopp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// Sentry organization slug&lt;/span&gt;
        &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tauri-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Sentry project name&lt;/span&gt;
        &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_AUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Auth token with proper scopes&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other config (e.g., resolve.alias, etc.)&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// crucial: generate source map files for production&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;ul&gt;
&lt;li&gt;A crucial step is to ignore the source-map files in the built app. In our case we utilized the &lt;a href="https://github.com/gethopp/hopp/blob/main/tauri/src-tauri/tauri.conf.json#L5" rel="noopener noreferrer"&gt;&lt;code&gt;beforeBuild&lt;/code&gt; hook&lt;/a&gt; to exclude them from the final bundle. It looks like this: &lt;code&gt;"beforeBuildCommand": "yarn build &amp;amp;&amp;amp; rm -rf ./dist/assets/*.js.map"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this setup, here’s what happens when we build our app:&lt;br&gt;
    1. Vite generates the JavaScript bundles and the corresponding &lt;code&gt;.map&lt;/code&gt; files (thanks to &lt;code&gt;sourcemap: true&lt;/code&gt;).&lt;br&gt;
    2. The Sentry Vite plugin kicks in at the end of the build, uploading those &lt;code&gt;.map&lt;/code&gt; files to Sentry.&lt;br&gt;
    3. We remove the source-map files from the build directory.&lt;br&gt;
    3.  We publish our app without including the &lt;code&gt;.map&lt;/code&gt; files in the bundle (the end-user package stays &lt;code&gt;~12 MB&lt;/code&gt;, no increase).&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying It Works (Readable Errors at Last!)
&lt;/h2&gt;

&lt;p&gt;With our code deployed using this new setup, we wanted to make sure everything was working correctly. To test it, we added a little snippet in our app that intentionally throws an error when a button is clicked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleSound&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPlaying&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stop call sound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Play call sound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Oh no an error&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Test Sentry Error
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run the app and click the &lt;code&gt;Test Sentry Error&lt;/code&gt; button, it triggers an error &lt;code&gt;("Oh no an error")&lt;/code&gt;. This error is reported to Sentry by our configured Sentry React SDK. Now comes the moment of truth: checking Sentry’s dashboard for this error.&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%2Fdlh49gjxx49i3.cloudfront.net%2Fsentry-sourcemaps%2FSentryAfter.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%2Fdlh49gjxx49i3.cloudfront.net%2Fsentry-sourcemaps%2FSentryAfter.png" alt="Sentry error stacktrace with source map applied, showing original source code lines" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice in the screenshot how Sentry provides the function name and source code snippet where the error occurred. This is possible only because Sentry could map the minified code back to our original TypeScript/JSX, thanks to the uploaded source map. Without the source map, we would have just seen something like &lt;code&gt;Error: Oh no an error&lt;/code&gt; at &lt;code&gt;app.bundle.js:1:10567&lt;/code&gt;, which is not actionable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating source maps with Sentry, we’ve drastically improved our debugging experience in production. Instead of cryptic stack traces, we get clear insights into errors as if we were running the app in development. The Sentry Vite plugin made this fairly straightforward to set up, handling the heavy lifting of uploading maps and associating them with releases.&lt;/p&gt;

&lt;p&gt;In our case, this setup has already paid off. We can track down issues in &lt;a href="https://gethopp.app/" rel="noopener noreferrer"&gt;Hopp&lt;/a&gt;’s app much faster now, because the errors tell us exactly where to look. If you’re using Sentry for a JavaScript app, take advantage of source maps and your future self debugging a production issue will thank you!&lt;/p&gt;

&lt;p&gt;If you have questions or want to discuss source-maps and debugging further, feel free to reach out on &lt;a href="https://twitter.com/costasAlexoglou/" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt; or email me directly at &lt;a href="//mailto:costa@gethopp.app"&gt;costa@gethopp.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>sentry</category>
    </item>
    <item>
      <title>Tauri vs. Electron: performance, bundle size, and the real trade-offs</title>
      <dc:creator>Iason Paraskevopoulos</dc:creator>
      <pubDate>Mon, 07 Jul 2025 14:18:22 +0000</pubDate>
      <link>https://forem.com/gethopp/tauri-vs-electron-performance-bundle-size-and-the-real-trade-offs-1el4</link>
      <guid>https://forem.com/gethopp/tauri-vs-electron-performance-bundle-size-and-the-real-trade-offs-1el4</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;At Hopp, we're building a cross-platform remote control application designed for a low-latency remote pair programming experience. Providing the best possible user experience is our top priority.&lt;/p&gt;

&lt;p&gt;Early in the development journey, the decision came to use either &lt;a href="https://tauri.app/" rel="noopener noreferrer"&gt;Tauri&lt;/a&gt; or &lt;a href="https://www.electronjs.org/" rel="noopener noreferrer"&gt;Electron&lt;/a&gt; to build the app. Using one of these frameworks offered a promising way to avoid writing native code for each platform (Windows, macOS, Linux). This meant a crucial choice needed consideration.&lt;/p&gt;

&lt;p&gt;Both Tauri and Electron offer robust frameworks with fundamental differences. We needed to carefully consider these differences to decide which framework would best serve Hopp long-term.&lt;/p&gt;

&lt;p&gt;If you currently weigh Tauri against Electron for your project, this post might help.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this post covers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The core differences between Tauri and Electron under the hood.&lt;/li&gt;
&lt;li&gt;  The advantages and limitations associated with each framework.&lt;/li&gt;
&lt;li&gt;  A benchmark comparing app size, startup time, and memory usage.&lt;/li&gt;
&lt;li&gt;  The reasoning behind choosing Tauri for Hopp.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The main differences between Tauri and Electron
&lt;/h2&gt;

&lt;p&gt;When developers first research Tauri, they often encounter articles describing it as a &lt;strong&gt;"lighter Electron"&lt;/strong&gt; or emphasizing the need to &lt;strong&gt;"learn Rust."&lt;/strong&gt; While truth exists in these points, they don't capture the full picture. These two leading cross-platform frameworks have architectural differences that impact development and performance.&lt;/p&gt;

&lt;p&gt;Let's dive into how Tauri and Electron operate behind the scenes.&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FElectronProcesses.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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FElectronProcesses.png" alt="Electron's model" width="800" height="395"&gt;&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FTauriProcesses.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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FTauriProcesses.png" alt="Tauri's model" width="800" height="725"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Electron's main process
&lt;/h3&gt;

&lt;p&gt;Electron's &lt;b&gt;main process runs as a NodeJS process&lt;/b&gt;. This architecture necessitates shipping a Node.js runtime with your app to ensure it runs correctly on any user's machine.&lt;/p&gt;

&lt;p&gt;As a result, your app's event handling relies on the &lt;a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick" rel="noopener noreferrer"&gt;Node.js event loop&lt;/a&gt;. For higher performance tasks or deeper OS-level interactions, you typically must spawn a separate process and establish communication via Inter-Process Communication (IPC) or another protocol like Unix Sockets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Electron's renderer process
&lt;/h3&gt;

&lt;p&gt;Think of the &lt;strong&gt;renderer process as a single browser tab spawned by the main process&lt;/strong&gt;. In practice, every window you open in an Electron app creates a new renderer process.&lt;/p&gt;

&lt;p&gt;This means an app with many windows will run numerous renderer processes concurrently. Developers must account for the cumulative memory and CPU usage of these processes—akin to running mini-Chromium instances in the background.&lt;/p&gt;

&lt;p&gt;The Electron team offers a helpful analogy:&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%2Fwww.electronjs.org%2Fassets%2Fimages%2Fchrome-processes-0506d3984ec81aa39985a95e7a29fbb8.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%2Fwww.electronjs.org%2Fassets%2Fimages%2Fchrome-processes-0506d3984ec81aa39985a95e7a29fbb8.png" alt="Electron's renderer process" width="593" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tauri main process
&lt;/h3&gt;

&lt;p&gt;Tauri leverages Rust for its backend. A key advantage comes from Rust compiling to a native binary, &lt;strong&gt;eliminating the need to bundle a runtime (like Node.js)&lt;/strong&gt; with the app. This contributes to a more lightweight app, though it involves trade-offs discussed later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tauri WebView process
&lt;/h3&gt;

&lt;p&gt;Instead of bundling the entire Chromium engine, Tauri uses the &lt;strong&gt;operating system's native WebView component&lt;/strong&gt; to render the UI. This component generally weighs less than a full browser engine. Currently, Tauri uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://learn.microsoft.com/en-us/microsoft-edge/webview2/" rel="noopener noreferrer"&gt;WebView2&lt;/a&gt; on Windows (based on Microsoft Edge/Chromium)&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://developer.apple.com/documentation/webkit/wkwebview" rel="noopener noreferrer"&gt;WKWebView&lt;/a&gt; on macOS (based on Safari/WebKit)&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://webkitgtk.org/" rel="noopener noreferrer"&gt;WebKitGTK&lt;/a&gt; on Linux&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By relying on the system's WebView, &lt;strong&gt;Tauri creates smaller app bundles, but this comes at a cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers using Tauri face potential challenges with cross-platform UI consistency. Browser-specific quirks can appear—issues in Safari but not Chrome, or Firefox behaving differently across operating systems. Since Tauri uses different underlying web engines on each OS, these platform variations become factors developers must manage during app development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features comparison
&lt;/h2&gt;

&lt;p&gt;Below is a comparison table highlighting key aspects of Tauri and Electron, with a slight emphasis on factors important to us at Hopp. Following the table, I'll detail a simple benchmark comparing size, startup time, and memory usage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Tauri 🦀&lt;/th&gt;
&lt;th&gt;Electron ⚛️&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup time&lt;/td&gt;
&lt;td&gt;🏎️ Fast&lt;/td&gt;
&lt;td&gt;🏎️ Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory Usage (Benchmark)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;172 MB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;409 MB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser technology&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WebView&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Chromium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend language&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Rust&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JavaScript&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build time (Initial)&lt;/td&gt;
&lt;td&gt;🐢 Slow&lt;/td&gt;
&lt;td&gt;🏎️ Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size (Benchmark)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8.6 MiB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;244 MiB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  App benchmark
&lt;/h2&gt;

&lt;p&gt;To illustrate the practical differences, this benchmark compares two apps using each framework. Their function:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Display a main window with a button to open a new window.&lt;/li&gt;
&lt;li&gt; Open 6 windows simultaneously to observe resource usage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following commands scaffolded the apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For Electron&lt;/span&gt;
npx create-electron-app@latest electron-demo-app &lt;span class="nt"&gt;--template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vite-typescript

&lt;span class="c"&gt;# For Tauri (selected TypeScript with Vite and React)&lt;/span&gt;
yarn create tauri-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting applications looked like this:&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FElectron_app.jpeg" 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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FElectron_app.jpeg" alt="Electron's app" width="800" height="517"&gt;&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FTauri_app.jpeg" 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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FTauri_app.jpeg" alt="Tauri's app" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚧 &lt;strong&gt;Disclaimer:&lt;/strong&gt; This is a basic benchmark performed on my MacBook Pro with a single run (&lt;code&gt;N=1&lt;/code&gt;). Take the following numbers with a grain of salt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Build time
&lt;/h3&gt;

&lt;p&gt;The initial build time was significantly slower for Tauri, primarily due to the Rust compilation step. Subsequent builds are typically much faster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Electron&lt;/span&gt;
❯ &lt;span class="nb"&gt;time &lt;/span&gt;yarn make
...[LOGS]...
yarn make  13.23s user 1.55s system 93% cpu 15.818 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Tauri&lt;/span&gt;
❯ &lt;span class="nb"&gt;time &lt;/span&gt;yarn tauri build
...[LOGS]....
yarn tauri build  380.11s user 28.37s system 504% cpu 1:20.94 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bundle size
&lt;/h3&gt;

&lt;p&gt;As expected from the architectural differences, Tauri produces a smaller app bundle. This occurs because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Tauri doesn't bundle a JavaScript runtime (Node.js).&lt;/li&gt;
&lt;li&gt;  Tauri uses the system's WebView instead of bundling Chromium.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference shows clearly below, from &lt;code&gt;8.6MiB&lt;/code&gt; for Tauri to &lt;code&gt;244MiB&lt;/code&gt; for Electron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ &lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; 1 &lt;span class="nb"&gt;.&lt;/span&gt;
8.6M    ./tauri-demo-app.app
244M    ./electron-demo-app.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FSize_Diff.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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FSize_Diff.png" alt="Bundle size comparison" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory usage
&lt;/h3&gt;

&lt;p&gt;This area highlights Tauri's "lighter" reputation. The difference stems from two main areas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Electron's Main Process:&lt;/strong&gt; The Node.js runtime inherently consumes memory.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Renderer Processes:&lt;/strong&gt; On macOS, Electron's Chromium-based renderer processes consumed roughly double the memory of Tauri's WKWebView processes for the same window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After opening 6 windows in each app, the approximate memory usage measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Tauri:&lt;/strong&gt; &lt;code&gt;~172 MB&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Electron:&lt;/strong&gt; &lt;code&gt;~409 MB&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FElectron_memory.jpeg" 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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FElectron_memory.jpeg" alt="Electron's memory usage" width="800" height="490"&gt;&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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FTauri_memory.jpeg" 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%2Fdlh49gjxx49i3.cloudfront.net%2Ftauri-vs-electron%2FTauri_memory.jpeg" alt="Tauri's memory usage" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Startup time
&lt;/h3&gt;

&lt;p&gt;Startup time is another common consideration. In this simple benchmark, the difference was negligible. Honestly, basing a framework decision solely on a startup time difference of less than even &lt;code&gt;1 500 ms&lt;/code&gt; is likely overthinking it for most applications.&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%2Fye8vaxcf3l4g30bi3qak.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%2Fye8vaxcf3l4g30bi3qak.gif" alt="Start up compare time" width="1728" height="1079"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more detailed benchmark data across frameworks, check out the &lt;a href="https://github.com/Elanis/web-to-desktop-framework-comparison#benchmarks" rel="noopener noreferrer"&gt;Web to Desktop Framework Comparison repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we chose Tauri for Hopp
&lt;/h2&gt;

&lt;p&gt;The comparison above presents the trade-offs between Tauri and Electron. For Hopp, the decision tilted towards Tauri for these key reasons:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Backend performance
&lt;/h3&gt;

&lt;p&gt;Hopp relies on a customized version of &lt;a href="https://github.com/iparaskev/rust-sdks/tree/main/libwebrtc" rel="noopener noreferrer"&gt;WebRTC&lt;/a&gt; to achieve ultra-low latency screen sharing. The app needs to stream video directly from a backend process, not using the browser's standard screen-sharing Application Programming Interfaces. &lt;/p&gt;

&lt;p&gt;Rust's performance suits this intensive task exceptionally well. Implementing this in Electron would require managing a separate process for the video stream, complicating the architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sidecar support
&lt;/h3&gt;

&lt;p&gt;While Tauri's main process uses Rust, Hopp needs a separate process (a "sidecar") for handling screen streaming and remote control input (clicks, typing). This allows for developing and testing this component separately from the rest of the app.&lt;/p&gt;

&lt;p&gt;Tauri's built-in &lt;a href="https://v2.tauri.app/develop/sidecar/" rel="noopener noreferrer"&gt;Sidecar concept&lt;/a&gt; simplifies managing the lifecycle of this external binary. Achieving the same in Electron would involve manually spawning, monitoring, and communicating with an external process, adding complexity. &lt;/p&gt;

&lt;p&gt;Note: Tauri's Sidecar differs conceptually from the Kubernetes pattern but serves a similar purpose of managing a companion process.&lt;/p&gt;

&lt;p&gt;We also utilize the sidebar process to draw the controller's cursor. In the future, we could swap to use &lt;a href="https://v2.tauri.app/blog/tauri-egui-0-1/" rel="noopener noreferrer"&gt;Tauri egui&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Feature parity:
&lt;/h3&gt;

&lt;p&gt;Although Tauri is newer than Electron, it's rapidly evolving. Tauri v2, in particular, is closing the feature gap, incorporating essential functionalities like a built-in &lt;a href="https://v2.tauri.app/plugin/updater/" rel="noopener noreferrer"&gt;updater&lt;/a&gt; as a first-class citizen. The project's momentum and focus on performance and security aligned well with our needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope this comparison has been insightful, especially if you're navigating the choice between Tauri and Electron for your next project.&lt;/p&gt;

&lt;p&gt;Ultimately, there's no single "right" answer. The best choice depends entirely on your specific use case, team expertise, and project requirements. Both frameworks are powerful tools capable of building excellent desktop applications, each with its distinct strengths and weaknesses.&lt;/p&gt;

&lt;p&gt;If you have questions or want to discuss this further, feel free to reach out on &lt;a href="https://twitter.com/costasAlexoglou/" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt; or email me directly at &lt;a href="//mailto:costa@gethopp.app"&gt;costa@gethopp.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>tauri</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
