<?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: Daniel Martinez Morgado</title>
    <description>The latest articles on DEV Community by Daniel Martinez Morgado (@dmartinezzm).</description>
    <link>https://dev.to/dmartinezzm</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%2F4234787%2Ffc573b8d-b0fd-4360-b7ac-097556181cce.jpg</url>
      <title>DEV Community: Daniel Martinez Morgado</title>
      <link>https://dev.to/dmartinezzm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmartinezzm"/>
    <language>en</language>
    <item>
      <title>I benchmarked 3 Angular timeline libraries with 10,000 bars</title>
      <dc:creator>Daniel Martinez Morgado</dc:creator>
      <pubDate>Mon, 09 Nov 2026 10:09:36 +0000</pubDate>
      <link>https://dev.to/dmartinezzm/i-benchmarked-3-angular-timeline-libraries-with-10000-bars-440n</link>
      <guid>https://dev.to/dmartinezzm/i-benchmarked-3-angular-timeline-libraries-with-10000-bars-440n</guid>
      <description>&lt;p&gt;I wrote ngx-lanes, so take this with the right amount of salt: I'm not a neutral party. That's why everything below is reproducible with three commands, and why I've tried hard to give the other two libraries a fair setup.&lt;/p&gt;

&lt;p&gt;The question: &lt;strong&gt;how do Angular timeline libraries cope with 1,000 resources and 10,000 bars?&lt;/strong&gt; That's a realistic size for planning screens: a fleet over two months, a hospital rota, a factory floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ngx-lanes 0.1.1&lt;/strong&gt;: my resource timeline. Rows are resources, bars are assignments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@worktile/gantt 22.0.0 (ngx-gantt)&lt;/strong&gt;: the most established open-source Angular Gantt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;schedula-core-angular 1.4.0&lt;/strong&gt;: the official Angular wrapper of SchedulaCore, a resource-based Gantt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The same 10,000 bars for all three, generated with a fixed seed.&lt;/li&gt;
&lt;li&gt;The same density: about 8 days per screen.&lt;/li&gt;
&lt;li&gt;Each library configured the way its docs recommend. For ngx-gantt that means &lt;code&gt;virtualScrollEnabled&lt;/code&gt; and &lt;code&gt;NgxGanttModule&lt;/code&gt;; for schedula-core, animations off.&lt;/li&gt;
&lt;li&gt;A production build with Angular 22, headless Chrome, a 1400 × 850 viewport, median of 3 runs.&lt;/li&gt;
&lt;li&gt;CPU throttled 4× in Chrome to approximate a modest laptop. A fast machine hides problems your users will see.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I measured four things: time until the first bar is painted, DOM nodes after that, frame times while scrolling with the mouse wheel, and how long the main thread is blocked during the scroll.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results (CPU throttled 4×)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Load&lt;/th&gt;
&lt;th&gt;DOM nodes&lt;/th&gt;
&lt;th&gt;Frame p95&lt;/th&gt;
&lt;th&gt;Slow frames&lt;/th&gt;
&lt;th&gt;Main thread blocked while scrolling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ngx-lanes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.46 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;344&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.8 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ngx-gantt&lt;/td&gt;
&lt;td&gt;1.07 s&lt;/td&gt;
&lt;td&gt;417&lt;/td&gt;
&lt;td&gt;49.9 ms&lt;/td&gt;
&lt;td&gt;6.6%&lt;/td&gt;
&lt;td&gt;1.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schedula-core&lt;/td&gt;
&lt;td&gt;66 s&lt;/td&gt;
&lt;td&gt;186,721&lt;/td&gt;
&lt;td&gt;1,100 ms&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;159 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without throttling, ngx-lanes and ngx-gantt both scroll at a perfect 60 fps; the difference is load time (0.15 s against 0.30 s). schedula-core still takes 14 s to show the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What explains the gap
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;schedula-core renders everything.&lt;/strong&gt; All 10,000 bars and their decorations are in the DOM from the start: 186,721 elements. Every scroll then has to move all of them. It's a fine design for a few hundred bars; at this size it collapses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ngx-gantt virtualizes rows, but it's a task Gantt.&lt;/strong&gt; It shows one bar per row, so 10,000 bars become 10,000 rows under 1,000 resource headers. Its virtualization keeps the DOM small, yet a screen shows about 35 bars where ngx-lanes shows about 110. Even rendering a third of the bars, it blocks the main thread for 1.4 s across the scroll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ngx-lanes virtualizes in both directions.&lt;/strong&gt; It only renders rows within the viewport (plus a margin of four), and within those rows only bars within 300 px of the visible time range. Finding those bars is a binary search per resource, not a scan. The size of the DOM depends on what's on screen, not on how many bars the plan has. I explained the technique in &lt;a href="https://dev.to/dmartinezzm/rendering-10000-bars-in-an-angular-timeline-without-freezing-the-ui-15j7"&gt;my previous post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the others win
&lt;/h2&gt;

&lt;p&gt;Speed isn't everything, and ngx-lanes is new. Today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ngx-gantt&lt;/strong&gt; has dependency links, drag and drop, a tree table, baselines and years of production use. If you need a project Gantt with those features, it's a solid choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;schedula-core&lt;/strong&gt; ships themes, a built-in editing popup and, in its paid version, drag and drop, calendars and context menus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ngx-lanes&lt;/strong&gt; is a read-only timeline for now. Drag and drop, conflict detection and live updates are on my roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Everything is in the repo: the app, the runner script and the raw results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/dmartinezzm/ngx-lanes
&lt;span class="nb"&gt;cd &lt;/span&gt;ngx-lanes/benchmarks
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
npm run bench &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--runs&lt;/span&gt; 3 &lt;span class="nt"&gt;--chrome&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/chrome"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One practical note: schedula-core-angular 1.4.0 is published without entry points in its &lt;code&gt;package.json&lt;/code&gt;, so Angular can't import it as is. The benchmark adds them with a small script that points at the package's own build output.&lt;/p&gt;

&lt;p&gt;If you think a library is misconfigured, open an issue or a pull request and I'll rerun the numbers. And if you build planning screens in Angular, I'd love to hear which of these limits you've hit.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>performance</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Rendering 10,000 bars in an Angular timeline without freezing the UI</title>
      <dc:creator>Daniel Martinez Morgado</dc:creator>
      <pubDate>Mon, 09 Nov 2026 09:31:52 +0000</pubDate>
      <link>https://dev.to/dmartinezzm/rendering-10000-bars-in-an-angular-timeline-without-freezing-the-ui-15j7</link>
      <guid>https://dev.to/dmartinezzm/rendering-10000-bars-in-an-angular-timeline-without-freezing-the-ui-15j7</guid>
      <description>&lt;p&gt;Planning screens are where Angular apps go to die: a row per vehicle, person or machine, a bar per booking, and suddenly the page holds tens of thousands of elements. I build this kind of screen at work, so I wrote &lt;strong&gt;ngx-lanes&lt;/strong&gt;, an open-source resource timeline that keeps only what you can see in the DOM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65flfwlb5uavlvzw2r0z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65flfwlb5uavlvzw2r0z.gif" alt="ngx-lanes scrolling 1,000 resources and 10,000 bars" width="720" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With 1,000 resources and 10,000 bars, the DOM holds about 22 rows and 50 to 130 bars at any moment. Here is how.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Index bars per resource, sorted by start
&lt;/h2&gt;

&lt;p&gt;The first step happens once per data change, inside a &lt;code&gt;computed()&lt;/code&gt;: group the bars by resource and sort them by start time. While grouping, keep the longest duration in each resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Find visible bars with a binary search
&lt;/h2&gt;

&lt;p&gt;A bar is visible if it overlaps the window &lt;code&gt;[from, to)&lt;/code&gt;. Sorted by start, the bars that can overlap begin at &lt;code&gt;from - maxDuration&lt;/code&gt; at the earliest, so a binary search jumps there and a short loop stops at the first bar that starts after &lt;code&gt;to&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lowerBound&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="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;maxDuration&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;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &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;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;result&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;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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;No bar outside the window is ever touched, so a resource with 10,000 bars costs about the same as one with 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Virtualize rows and time
&lt;/h2&gt;

&lt;p&gt;Rows are absolutely positioned at &lt;code&gt;index * rowHeight&lt;/code&gt; inside a canvas as tall as all rows together, so the scrollbar is real. From &lt;code&gt;scrollTop&lt;/code&gt; and the viewport height I compute the visible rows plus four rows of margin. The same idea works horizontally: only bars within 300 px of the viewport are rendered, and only the time ticks you can see.&lt;/p&gt;

&lt;p&gt;Everything is a chain of signals: &lt;code&gt;scrollTop&lt;/code&gt;, &lt;code&gt;scrollLeft&lt;/code&gt; and the viewport size feed &lt;code&gt;computed()&lt;/code&gt; values for the visible window, the ticks and the rows. The template loops over the result with &lt;code&gt;@for&lt;/code&gt; and &lt;code&gt;track&lt;/code&gt;, so Angular reuses the elements that stay on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep the same instant centred when zooming
&lt;/h2&gt;

&lt;p&gt;Switching from month to day zoom makes the canvas 36 times wider. If you set &lt;code&gt;scrollLeft&lt;/code&gt; straight away, the browser clamps it to the old width and you land somewhere else. The fix is to wait for the render that resizes the canvas:&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="nf"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&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="nf"&gt;scale&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;anchor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;untracked&lt;/span&gt;&lt;span class="p"&gt;(()&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="nx"&gt;anchorTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;afterNextRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;write&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setScrollLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchor&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="nf"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;width&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;injector&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;injector&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;One more trap: a scroll you set from code fires its &lt;code&gt;scroll&lt;/code&gt; event later, asynchronously. If that event updates the anchor, a clamped value overwrites the instant you wanted to keep, so programmatic scrolls are flagged and their event is ignored.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Demo with 1,000 resources and 10,000 bars: &lt;a href="https://dmartinezzm.github.io/ngx-lanes/" rel="noopener noreferrer"&gt;https://dmartinezzm.github.io/ngx-lanes/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm install ngx-lanes&lt;/code&gt; (Angular 19+, zoneless or zone.js, MIT)&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/dmartinezzm/ngx-lanes" rel="noopener noreferrer"&gt;https://github.com/dmartinezzm/ngx-lanes&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm working on a Pro version with drag and drop, conflict detection and live updates from SignalR. If your team builds planning screens, the waitlist is on the demo site, and I'd like to hear what you plan with it.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>performance</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
