<?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: Smaug#6739</title>
    <description>The latest articles on DEV Community by Smaug#6739 (@smaug6739).</description>
    <link>https://dev.to/smaug6739</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%2F2042020%2F274920d0-62a3-4961-8839-a933bbeb800a.jpg</url>
      <title>DEV Community: Smaug#6739</title>
      <link>https://dev.to/smaug6739</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smaug6739"/>
    <language>en</language>
    <item>
      <title>How I made my Vue tree 10x faster without changing the UI</title>
      <dc:creator>Smaug#6739</dc:creator>
      <pubDate>Thu, 09 Jul 2026 13:00:17 +0000</pubDate>
      <link>https://dev.to/smaug6739/how-i-made-my-vue-tree-10x-faster-without-changing-the-ui-570a</link>
      <guid>https://dev.to/smaug6739/how-i-made-my-vue-tree-10x-faster-without-changing-the-ui-570a</guid>
      <description>&lt;p&gt;When I started optimizing the frontend of my knowledge base, I assumed tree generation would be the bottleneck.&lt;/p&gt;

&lt;p&gt;It wasn't. The trees themselves were fast.&lt;/p&gt;

&lt;p&gt;The real culprit? &lt;strong&gt;Vue's reactive system was constantly rebuilding them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By rethinking how data was stored and controlled, I managed to drop the initial tree computation time &lt;strong&gt;from 270ms to 27ms (a 10x improvement)&lt;/strong&gt;, without touching a single line of UI code. Here's the architecture that made it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  It all started with three tables
&lt;/h2&gt;

&lt;p&gt;It all started with three database tables.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Categories&lt;/li&gt;
&lt;li&gt;Documents&lt;/li&gt;
&lt;li&gt;Uploads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each represented a different type of object, each had its own API, and each had its own frontend store.&lt;/p&gt;

&lt;p&gt;Everything looked perfectly reasonable... until I looked at the schema more closely.&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%2Fxgn4e9hg2id3hgcprhrk.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%2Fxgn4e9hg2id3hgcprhrk.png" alt="First architecture" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite being different entities, they all had almost the same properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="nx"&gt;description&lt;/span&gt;
&lt;span class="nx"&gt;parent_id&lt;/span&gt;
&lt;span class="nx"&gt;icon&lt;/span&gt;
&lt;span class="nx"&gt;color&lt;/span&gt;
&lt;span class="nx"&gt;created_timestamp&lt;/span&gt;
&lt;span class="nx"&gt;updated_timestamp&lt;/span&gt;
&lt;span class="c1"&gt;// And more...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only real difference was what they represented.&lt;/p&gt;

&lt;p&gt;I realized that it would be much simpler if all these entities could be combined. This would avoid duplicating code, routes, the store, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  From three tables to one graph
&lt;/h2&gt;

&lt;p&gt;Instead of storing categories, documents and uploads separately, I merged everything into a single &lt;code&gt;nodes&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Every object became a node with a specific type.&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%2Fulv910u2o0nkmy0bubu2.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%2Fulv910u2o0nkmy0bubu2.png" alt="New nodes table" width="445" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, the idea felt almost too simple.&lt;/p&gt;

&lt;p&gt;Of course, merging several tables into one isn't free. Some columns inevitably become irrelevant for certain node types. For example, a category doesn't have any content, while a document does.&lt;/p&gt;

&lt;p&gt;But in practice, this trade-off turned out to be much smaller than I expected. Empty columns take very little space in a relational database, and in return every node shares the same structure, the same API, and the same frontend model.&lt;/p&gt;

&lt;p&gt;I also found that using generic field names made the model surprisingly flexible. Instead of having fields like &lt;code&gt;document_content_md&lt;/code&gt; or &lt;code&gt;document_content_html&lt;/code&gt;, I renamed them to more generic names such as &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;content_compiled&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Different node types can then interpret those fields differently while keeping the same underlying schema. For a document, they store the Markdown source and its compiled HTML. For an uploaded image, the very same fields can store the original file URL and its optimized WebP version.&lt;/p&gt;

&lt;p&gt;The database schema became less specialized, but the frontend became dramatically simpler because every object follows the same contract.&lt;/p&gt;

&lt;p&gt;With this new single-table design, the backend was now sending a flat, unified stream of nodes to the frontend. It was elegant, but it shifted a massive burden onto the client: the frontend now had to organize this raw graph into readable structures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem wasn't SQL
&lt;/h2&gt;

&lt;p&gt;The UI, however, didn't need one graph.&lt;/p&gt;

&lt;p&gt;It needed many different trees.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;the workspace tree&lt;/li&gt;
&lt;li&gt;the sidebar hierarchy&lt;/li&gt;
&lt;li&gt;search results&lt;/li&gt;
&lt;li&gt;filtered views&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each represents the same data from a different perspective.&lt;/p&gt;

&lt;p&gt;The obvious solution would have been to build each tree independently.&lt;/p&gt;

&lt;p&gt;The graph solved one problem: every object now existed only once.&lt;/p&gt;

&lt;p&gt;But the UI still had another requirement.&lt;/p&gt;

&lt;p&gt;It wasn't enough to store the data efficiently.&lt;/p&gt;

&lt;p&gt;It also had to expose multiple independent tree views, all backed by the same graph and all fully reactive.&lt;/p&gt;

&lt;p&gt;That's where the real challenge began.&lt;/p&gt;

&lt;h2&gt;
  
  
  A graph is only half the solution
&lt;/h2&gt;

&lt;p&gt;At this point I had a nice data model.&lt;/p&gt;

&lt;p&gt;Everything was a node.&lt;/p&gt;

&lt;p&gt;There was a single source of truth.&lt;/p&gt;

&lt;p&gt;The backend was clean.&lt;/p&gt;

&lt;p&gt;Unfortunately, the frontend still wasn't.&lt;/p&gt;

&lt;p&gt;The problem wasn't building a tree.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Building a tree isn't particularly expensive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebuilding it hundreds of times is.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real problem was how often those trees were rebuilt.&lt;/p&gt;

&lt;p&gt;If every small modification causes reactive computations across the entire application, performance quickly becomes unpredictable.&lt;/p&gt;

&lt;p&gt;That completely changed the question I was trying to solve.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can I build trees efficiently?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can I make sure they are only rebuilt when they actually need to be?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  My first implementation
&lt;/h3&gt;

&lt;p&gt;Initially, every node lived inside a simple array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked perfectly... Until the project grew.&lt;/p&gt;

&lt;p&gt;Every lookup by ID became an O(n) operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding children&lt;/li&gt;
&lt;li&gt;Finding parents&lt;/li&gt;
&lt;li&gt;Updating nodes&lt;/li&gt;
&lt;li&gt;Checking existence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many operations slowly became &lt;code&gt;O(n)&lt;/code&gt; or even &lt;code&gt;O(n²)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The code stayed simple, but the data structure was no longer appropriate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The obvious improvement
&lt;/h3&gt;

&lt;p&gt;The next step was replacing the array with a &lt;code&gt;Map&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lookups instantly became O(1).&lt;/p&gt;

&lt;p&gt;Problem solved?&lt;/p&gt;

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

&lt;p&gt;Maps don't preserve a custom sort order, but the UI almost always required sorted nodes.&lt;/p&gt;

&lt;p&gt;So the code constantly looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of a clean reactive flow, the code was doing this on every render:&lt;/span&gt;
&lt;span class="nb"&gt;Map&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Sort&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complexity had simply moved somewhere else.&lt;/p&gt;

&lt;p&gt;Instead of paying for lookups, I was now paying for conversions and sorting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building an indexed collection
&lt;/h3&gt;

&lt;p&gt;Eventually I realized I didn't actually need another container.&lt;/p&gt;

&lt;p&gt;I needed a container that understood my access patterns.&lt;/p&gt;

&lt;p&gt;I built a small abstraction that keeps one copy of every node while maintaining lightweight indexes alongside it.&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%2Fw9edw44rl5752xb8nez3.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%2Fw9edw44rl5752xb8nez3.png" alt="Diagram of IndexedCollection" width="421" height="234"&gt;&lt;/a&gt;&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="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;byParent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&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="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="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;byRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&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="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;sortedArray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the indexes don't duplicate the nodes themselves. They only store IDs.&lt;/p&gt;

&lt;p&gt;The actual objects still exist only once.&lt;/p&gt;

&lt;p&gt;This turned out to be an important distinction because it keeps memory usage low while allowing different views of the same data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real optimization wasn't the indexes
&lt;/h3&gt;

&lt;p&gt;Surprisingly, the biggest performance leap didn't come from faster lookups.&lt;/p&gt;

&lt;p&gt;It came from controlling reactivity.&lt;/p&gt;

&lt;p&gt;Vue is extremely good at updating the UI.&lt;/p&gt;

&lt;p&gt;But if every insertion immediately triggers every dependent computation, even a good data structure becomes inefficient.&lt;/p&gt;

&lt;p&gt;Imagine loading a workspace containing 200 nodes.&lt;/p&gt;

&lt;p&gt;With a traditional reactive array, inserting those nodes one by one would notify every dependent computed property 200 times.&lt;/p&gt;

&lt;p&gt;The tree generation itself isn't expensive.&lt;/p&gt;

&lt;p&gt;Rebuilding it hundreds of times unnecessarily is.&lt;/p&gt;

&lt;p&gt;Instead of relying solely on Vue's default reactivity, the collection exposes its own lightweight dependency system.&lt;/p&gt;

&lt;p&gt;Internally, it tracks consumers using a single &lt;code&gt;shallowRef&lt;/code&gt;, and only notifies them when the collection reaches a consistent state.&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;class&lt;/span&gt; &lt;span class="nc"&gt;IndexedCollection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;isBatching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;_dependents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shallowRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isBatching&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;triggerRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_dependents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;startBulk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isBatching&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="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;endBulk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isBatching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rebuildSortedArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&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;That made it possible to introduce bulk operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Begin bulk update

Insert 200 nodes

Update indexes

Notify once

Recompute once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than triggering hundreds of updates during initialization, every dependent tree is rebuilt only once.&lt;/p&gt;

&lt;p&gt;The same principle applies throughout the application.&lt;/p&gt;

&lt;p&gt;Notifications are only emitted after actual CRUD operations that modify the database state.&lt;/p&gt;

&lt;p&gt;As a result, the frontend always stays synchronized with the backend, while avoiding unnecessary reactive work.&lt;/p&gt;

&lt;p&gt;The difference becomes obvious during the initial workspace loading.&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%2Ftyddwpvfyrl2dghu00z3.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%2Ftyddwpvfyrl2dghu00z3.png" alt="Performances: Comparing with and without controlling reactivity" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking back, the biggest optimization wasn't algorithmic. &lt;/p&gt;

&lt;p&gt;It was architectural: A different way of thinking about reactivity.&lt;/p&gt;

&lt;p&gt;Instead of asking "How can I make tree generation faster?", I started asking "How can I make sure it only happens when it's actually needed?"&lt;/p&gt;

&lt;p&gt;That single shift in perspective reduced the initial tree computation time from 270 ms to 27 ms, while keeping a single source of truth for the entire application.&lt;/p&gt;

&lt;p&gt;If you want to dive deeper into the code, this entire architecture is open-source! You can check out the full implementation inside the Alexandrie project on GitHub: &lt;strong&gt;&lt;a href="https://github.com/Smaug6739/Alexandrie" rel="noopener noreferrer"&gt;https://github.com/Smaug6739/Alexandrie&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you found this breakdown helpful, feel free to drop a ⭐️ on the repository or leave a comment below with how you handle complex reactivity bottlenecks in your apps!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Offline PWA with Nuxt: Making an existing Nuxt app work offline without rewriting it</title>
      <dc:creator>Smaug#6739</dc:creator>
      <pubDate>Wed, 08 Jul 2026 14:57:41 +0000</pubDate>
      <link>https://dev.to/smaug6739/making-an-existing-nuxt-app-work-offline-without-rewriting-it-lmh</link>
      <guid>https://dev.to/smaug6739/making-an-existing-nuxt-app-work-offline-without-rewriting-it-lmh</guid>
      <description>&lt;p&gt;Adding offline support to an existing application always sounded like one of those features that would require rewriting half the codebase.&lt;/p&gt;

&lt;p&gt;That was exactly why I kept postponing it.&lt;/p&gt;

&lt;p&gt;I imagined I'd have to sprinkle if (online) checks everywhere, duplicate logic in components, keep separate offline states, and generally make the whole application much harder to maintain.&lt;/p&gt;

&lt;p&gt;In the end... it turned out to be much simpler than I expected.&lt;/p&gt;

&lt;p&gt;The key wasn't a particular library it was keeping the architecture clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;I wanted a complete offline mode where users could continue working even without a network connection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browse existing data&lt;/li&gt;
&lt;li&gt;Create, edit and delete content&lt;/li&gt;
&lt;li&gt;Close the application&lt;/li&gt;
&lt;li&gt;Come back later
Automatically synchronize everything once they're back online&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, I didn't want my Vue components to know whether they were online or offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;I decided that components should never care where the data comes from.&lt;/p&gt;

&lt;p&gt;Instead, everything goes through the stores.&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%2F5zny5tzyzoh4cyzplwo6.webp" 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%2F5zny5tzyzoh4cyzplwo6.webp" alt="Diagram of architecture" width="311" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The component simply calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;nodesStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether that operation goes to the server immediately or is stored locally for later synchronization is entirely the store's responsibility. The API never changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading data
&lt;/h2&gt;

&lt;p&gt;For reads, I relied on PWA + Workbox.&lt;/p&gt;

&lt;p&gt;Requests are intercepted transparently and served from the cache whenever appropriate. When the application starts, previously synchronized data is loaded into the store's cache.&lt;/p&gt;

&lt;p&gt;From the store's perspective, nothing changes: it still fetches data exactly as before. If a piece of data isn't available locally, it simply can't be accessed offline.&lt;/p&gt;

&lt;p&gt;The components don't know whether the response came from the network or from Workbox. And with this approach we don't have to manage a full cache, the biggest part is handled by workbox, we just manually store content created or updated offline.&lt;/p&gt;

&lt;p&gt;Nothing changed in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing data
&lt;/h2&gt;

&lt;p&gt;CRUD operations are handled a little differently.&lt;/p&gt;

&lt;p&gt;When online, the store behaves normally.&lt;/p&gt;

&lt;p&gt;When offline, instead of calling the API, it stores the operation inside a small local database (IndexedDB, using localForage library) and immediately updates the local state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Component
     │
     ▼
 Store.create()
     │
Online?
 ├── Yes → API
 └── No  → Queue operation in IndexedDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, when the connection comes back, the queued operations are replayed automatically in the background in the same order.&lt;/p&gt;

&lt;p&gt;From the component's perspective, nothing changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest benefit
&lt;/h2&gt;

&lt;p&gt;The offline mode itself is nice.&lt;/p&gt;

&lt;p&gt;But the real win is that almost none of my components had to change.&lt;/p&gt;

&lt;p&gt;The entire application still interacts with the stores exactly the same way.&lt;/p&gt;

&lt;p&gt;The complexity stays in one place instead of leaking into every page and every component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;A few things surprised me while implementing this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It was much simpler than I expected&lt;/li&gt;
&lt;li&gt;I had postponed the feature for months because I thought every component would need special offline logic.&lt;/li&gt;
&lt;li&gt;In reality, combining PWA + Workbox for reads with a small local database and adapting the stores was enough.&lt;/li&gt;
&lt;li&gt;Keeping components completely unaware of the network state makes the application much easier to maintain.&lt;/li&gt;
&lt;li&gt;Good architecture often saves far more time than clever code.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Offline support ended up being one of the features I feared the most, but also one of the cleanest to implement once the architecture was in place.&lt;/p&gt;

&lt;p&gt;The complete implementation is part of Alexandrie, an open-source knowledge base built with Nuxt and Go. If you're curious about the full architecture or the implementation details, you can find the project here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Smaug6739/Alexandrie" rel="noopener noreferrer"&gt;https://github.com/Smaug6739/Alexandrie&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>webdev</category>
      <category>pwa</category>
    </item>
    <item>
      <title>Reactive Tree Management in Nuxt 4: How I Modeled Complex Hierarchies with Pinia</title>
      <dc:creator>Smaug#6739</dc:creator>
      <pubDate>Sat, 01 Nov 2025 12:17:22 +0000</pubDate>
      <link>https://dev.to/smaug6739/reactive-tree-management-in-nuxt-4-how-i-modeled-complex-hierarchies-with-pinia-2m8f</link>
      <guid>https://dev.to/smaug6739/reactive-tree-management-in-nuxt-4-how-i-modeled-complex-hierarchies-with-pinia-2m8f</guid>
      <description>&lt;p&gt;When I started building Alexandrie, I just wanted a fast, offline Markdown note-taking app I could rely on daily.&lt;br&gt;
But as the project grew  with nested categories, shared workspaces, and access permissions it evolved into something much more: a open-source knowledge management platform powered by Nuxt 4 and Go.&lt;/p&gt;

&lt;p&gt;This article walks through one of the toughest challenges I faced: how to model and manage hierarchical data efficiently, from the database to a reactive Pinia store.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Unified Data Model: “Nodes” Over Multiple Tables
&lt;/h2&gt;

&lt;p&gt;Early in development, I realized that managing &lt;strong&gt;categories, documents, and files&lt;/strong&gt; as separate entities was becoming painful.&lt;br&gt;&lt;br&gt;
Every new feature  especially &lt;strong&gt;sharing&lt;/strong&gt; and &lt;strong&gt;permissions&lt;/strong&gt; required deeper joins and complex recursive queries.&lt;/p&gt;

&lt;p&gt;Here’s what the early structure looked like conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workspace
 ├── Category A
 │    ├── Document 1
 │    └── Document 2
 └── Category B
      ├── Subcategory
      │    └── Document 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.1 The problem with separate tables
&lt;/h3&gt;

&lt;p&gt;Having multiple tables (&lt;code&gt;categories&lt;/code&gt;, &lt;code&gt;documents&lt;/code&gt;, &lt;code&gt;resources&lt;/code&gt;) worked fine until I introduced access control.&lt;br&gt;&lt;br&gt;
At that point, even simple questions like “what can this user see in this subtree?” required multiple recursive joins.&lt;br&gt;&lt;br&gt;
Performance and maintainability started to suffer.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.2 The unified “nodes” approach
&lt;/h3&gt;

&lt;p&gt;The solution was to merge everything into a &lt;strong&gt;single table&lt;/strong&gt; a unified model where everything is a &lt;code&gt;node&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="n"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="nb"&gt;SMALLINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;workspace&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="n"&gt;category&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="n"&gt;document&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="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;LONGTEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content_compiled&lt;/span&gt; &lt;span class="nb"&gt;LONGTEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this model, every element document, folder, file became a node.&lt;br&gt;
This made the hierarchy recursive but uniform, enabling simple queries like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What can user X access in this subtree?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, permission propagation and tree traversal both use a single recursive CTE or indexed path column, drastically improving maintainability and speed.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Scalable Permission System
&lt;/h2&gt;

&lt;p&gt;Building permissions on top of the nodes table required careful thought. I adopted a hybrid approach:&lt;/p&gt;

&lt;p&gt;A separate permissions table maps &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;node_id&lt;/code&gt;, &lt;code&gt;permission_level&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On access checks, the system checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user owns the target node&lt;/li&gt;
&lt;li&gt;If a direct permission exists&lt;/li&gt;
&lt;li&gt;If any ancestor node grants sufficient permission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This balances fine-grained control and inheritance users get access to everything under a node they’ve been granted permission for, without repeated recursive checks.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Backend Stack: Go + REST + Modular Design
&lt;/h2&gt;

&lt;p&gt;Language &amp;amp; framework: Go (Gin) lightweight and performant for API endpoints.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: Go (Gin) for its simplicity, performance, and clean REST design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: MySQL (or compatible) raw SQL for critical queries like subtree retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File storage&lt;/strong&gt;: S3-compatible (MinIO, RustFS) abstracted via a pluggable service for self-hosted setups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API uses a flat structure (&lt;code&gt;/nodes&lt;/code&gt;, &lt;code&gt;/permissions&lt;/code&gt;, &lt;code&gt;/users&lt;/code&gt;) simple, predictable, and easy to version.&lt;/p&gt;

&lt;p&gt;I separated business logic from data access (DAO layer) to keep the backend maintainable and extensible.&lt;br&gt;
This paid off when refactoring: new storage backends or permission engines can now be added with minimal changes.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Frontend Architecture: Data management
&lt;/h2&gt;

&lt;p&gt;Surprisingly, the hardest part of building Alexandrie’s frontend wasn’t the UI it was the data layer.&lt;br&gt;
Representing thousands of interlinked notes in a reactive, permission-aware tree required careful design.&lt;/p&gt;

&lt;p&gt;In Alexandrie, &lt;strong&gt;everything is a Node&lt;/strong&gt;.&lt;br&gt;
A node can be a workspace, category, document, or resource and each can contain others.&lt;br&gt;
That means infinite nesting, partial hydration, and live updates when any part of the tree changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Alexandrie, everything is a Node.&lt;br&gt;
Each node can be a workspace, category, document, or resource, and every node can contain others effectively forming a tree structure that must remain reactive, searchable, and permission-aware across the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The main challenges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nested data: Users can nest documents/categories/resources infinitely deep.&lt;/li&gt;
&lt;li&gt;Partial hydration: Nodes are often fetched lazily or shared publicly, so the store must handle both partial and fully-hydrated nodes.&lt;/li&gt;
&lt;li&gt;Permission inheritance: Access rights propagate through parent nodes.&lt;/li&gt;
&lt;li&gt;Real-time reactivity: Any node update must immediately reflect across trees, search results, and UI components.&lt;/li&gt;
&lt;li&gt;Performance: Traversing large trees shouldn’t cause noticeable slowdowns.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;I built a dedicated Pinia store (useNodesStore) combined with a TreeStructure utility that keeps all nodes in a flat Collection (essentially a reactive Map), and reconstructs the hierarchical tree on demand.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useNodesStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodes&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;state&lt;/span&gt;&lt;span class="p"&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="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Collection&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;allTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;isFetching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;getters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;getChilds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;getAllChildrens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&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="cm"&gt;/* recursive logic */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* lazy hydration of nodes */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* merges partial and full states */&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;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* removes entire subtree */&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;Then, a dedicated &lt;code&gt;TreeStructure&lt;/code&gt; class handles building the actual trees efficiently:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TreeStructure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;itemMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Item&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;private&lt;/span&gt; &lt;span class="nx"&gt;childrenMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Item&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[])&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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childrenMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childrenMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;||&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childrenMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent_id&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;generateTree&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buildTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&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;Set&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;This approach gives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O(1) access to any node via itemMap&lt;/li&gt;
&lt;li&gt;Efficient subtree generation (only rebuild what’s needed)&lt;/li&gt;
&lt;li&gt;A clean way to filter, search, or recompute derived data (tags, permissions, etc.)&lt;/li&gt;
&lt;li&gt;Integration with Pinia’s reactivity: the entire graph updates live when a node changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons &amp;amp; Trade-offs
&lt;/h2&gt;

&lt;p&gt;Here are some high-level takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Unify your data model early. Splitting multiple tables will bite you when adding features like permissions and sharing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Favor simplicity in API contracts. Flat endpoints scale better than deeply nested resource structures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Design for extensibility, not just immediate features. Adding plugin-like syntax blocks or alternate storage later becomes much easier.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Permissions and hierarchy are hard. Caching accessible node sets and flattening ancestors helps avoid recursive query bottlenecks. &lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next &amp;amp; How to Contribute
&lt;/h2&gt;

&lt;p&gt;Alexandrie is open-source and welcomes contributors. Areas where help is most welcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI/UX improvements&lt;/li&gt;
&lt;li&gt;Implement full offline support&lt;/li&gt;
&lt;li&gt;Add some cool new features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re interested in self-hosted knowledge tools or modern note apps, feel free to star or contribute!&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Smaug6739/Alexandrie" rel="noopener noreferrer"&gt;https://github.com/Smaug6739/Alexandrie&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>nuxt</category>
    </item>
    <item>
      <title>I built a Markdown note-taking app for students and creators — and I’d love your feedback</title>
      <dc:creator>Smaug#6739</dc:creator>
      <pubDate>Mon, 28 Jul 2025 11:51:27 +0000</pubDate>
      <link>https://dev.to/smaug6739/i-built-a-markdown-note-taking-app-for-students-and-creators-and-id-love-your-feedback-o0o</link>
      <guid>https://dev.to/smaug6739/i-built-a-markdown-note-taking-app-for-students-and-creators-and-id-love-your-feedback-o0o</guid>
      <description>&lt;p&gt;Hi👋&lt;/p&gt;

&lt;p&gt;Over the past few years, I’ve been working on a personal side project that turned into something I now use every day: Alexandrie — a note-taking app built with students, developers, and content creators in mind.&lt;/p&gt;

&lt;p&gt;I’m publishing this not as a product pitch, but as an open invitation to explore, test, break, and maybe even contribute to the app — especially if you're interested in performance-focused web apps, offline-first design, and clean UI/UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ What is Alexandrie?
&lt;/h2&gt;

&lt;p&gt;Alexandrie is a web-based Markdown note-taking app designed to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✍️ Minimal and elegant — because tools should get out of your way&lt;/li&gt;
&lt;li&gt;⚡ Lightweight and fast — built for speed, even with hundreds of documents&lt;/li&gt;
&lt;li&gt;🌐 Offline-friendly — your notes stay available even without a connection&lt;/li&gt;
&lt;li&gt;🛠️ Custom productivity features — snippets, keyboard shortcuts, instant formatting&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxj15hfgh9igtms0l5mlm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxj15hfgh9igtms0l5mlm.png" alt="Alexandrie app" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s designed first and foremost for students (like myself when I started it), but also fits the needs of developers and creators who want a frictionless writing and organization tool.&lt;/p&gt;

&lt;p&gt;🧠 The Philosophy Behind It&lt;br&gt;
I’ve tried tools like Notion, Obsidian, and many others. While they’re great, I often found them too heavy, too slow, or visually cluttered for daily academic or technical writing. And plain Markdown editors often lack structure and comfort.&lt;/p&gt;

&lt;p&gt;I wanted a tool that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is visually pleasant (especially for long writing sessions)&lt;/li&gt;
&lt;li&gt;Respects performance and runs well even on modest devices&lt;/li&gt;
&lt;li&gt;Works offline by default (ideal in classrooms, on trains, in bad Wi-Fi)&lt;/li&gt;
&lt;li&gt;Handles hundreds of notes without slowing down&lt;/li&gt;
&lt;li&gt;Supports shortcuts, snippets, and fast navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🧰 Tech Stack&lt;br&gt;
Alexandrie is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Vue.js + Nuxt 3 (SSR, PWA-ready)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Go, REST API, simple auth system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: MinIO for file/media uploads (e.g. images in notes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: MySQL, with raw SQL queries separated from business logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline mode&lt;/strong&gt;: Service Workers for seamless note access&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧪 Things I’m still improving
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Better search and filtering&lt;/li&gt;
&lt;li&gt;Better UI and UX with animations etc&lt;/li&gt;
&lt;li&gt;Full PWA installation support&lt;/li&gt;
&lt;li&gt;User settings &amp;amp; themes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  💡 Want to contribute ?
&lt;/h2&gt;

&lt;p&gt;If you’re interested in open source, Alexandrie is a great playground for:&lt;/p&gt;

&lt;p&gt;Vue/Nuxt 3 devs who like clean UIs and components&lt;/p&gt;

&lt;p&gt;Go backend developers who care about simplicity and performance&lt;/p&gt;

&lt;p&gt;People who love improving documentation or writing tutorials&lt;/p&gt;

&lt;p&gt;UX enthusiasts with ideas on improving offline-first apps&lt;/p&gt;

&lt;p&gt;You can also just drop feedback, file bugs, or suggest features — I read everything!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 GitHub: &lt;a href="https://github.com/Smaug6739/Alexandrie" rel="noopener noreferrer"&gt;https://github.com/Smaug6739/Alexandrie&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some app features images&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Focamrywdy7rvs51t1orj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Focamrywdy7rvs51t1orj.png" alt=" " width="800" height="450"&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2vv4p36h8e4nqg2kkma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2vv4p36h8e4nqg2kkma.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tooling</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
