<?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: Nikolas Howard</title>
    <description>The latest articles on DEV Community by Nikolas Howard (@nik_f289a9240080d43eb7ee9).</description>
    <link>https://dev.to/nik_f289a9240080d43eb7ee9</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%2F3179713%2F7f184187-6b8f-481b-9f26-fca50fdc207e.png</url>
      <title>DEV Community: Nikolas Howard</title>
      <link>https://dev.to/nik_f289a9240080d43eb7ee9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nik_f289a9240080d43eb7ee9"/>
    <language>en</language>
    <item>
      <title>I got tired of laggy timelines, so I've been using Tempis</title>
      <dc:creator>Nikolas Howard</dc:creator>
      <pubDate>Sat, 24 Oct 2026 14:23:04 +0000</pubDate>
      <link>https://dev.to/nik_f289a9240080d43eb7ee9/i-got-tired-of-laggy-timelines-so-ive-been-using-tempis-5di0</link>
      <guid>https://dev.to/nik_f289a9240080d43eb7ee9/i-got-tired-of-laggy-timelines-so-ive-been-using-tempis-5di0</guid>
      <description>&lt;p&gt;Every few months I end up needing a timeline in some app. A scheduling view, a project plan, a feed of events plotted against time. And every few months I rediscover that most timeline components fall apart once you give them real data. Fifty items, fine. A couple thousand? Scroll goes choppy, memory creeps up, and hovering over anything feels like wading through syrup.&lt;/p&gt;

&lt;p&gt;The reason is usually the same. These libraries keep a DOM node for every single item. The browser is great at a lot of things, but keeping thousands of absolutely-positioned divs in sync while you pan around is not really one of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tempis.dev" rel="noopener noreferrer"&gt;Tempis&lt;/a&gt; takes the other route and draws everything to a canvas. One element, no per-item nodes. So the question of "how many items can I show" stops being about your layout engine and starts being about how much you actually want on screen. It's held up fine for me past 5,000 items, and the whole thing is under 18 KB gzipped with zero dependencies, which I appreciated more than I expected to. Nothing else to audit, nothing else to break.&lt;/p&gt;

&lt;p&gt;Here's the smallest thing that does something useful:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TempisTimeline&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;@tempis/timeline&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;timeline&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;TempisTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#canvas&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;responsive&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="na"&gt;items&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Design&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grouping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Frontend&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Build&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grouping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Frontend&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Launch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-30&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grouping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Frontend&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-08&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-25&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grouping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Backend&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;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-02-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bottom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An item with a &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; is a range. An item with just a &lt;code&gt;start&lt;/code&gt; is a point in time. When they overlap, they stack themselves, so you're not doing lane-assignment math by hand. The &lt;code&gt;grouping&lt;/code&gt; field is what splits those four items into a Frontend row and a Backend row, and you can collapse groups, sort them your own way, and let people click a legend to filter by category. That legend-plus-grouping combo is the thing I reach for most, honestly. It turns a flat wall of bars into something you can actually read.&lt;/p&gt;

&lt;p&gt;If you're not on a build step, there's a CDN drop-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@tempis/timeline/dist/tempis_timeline.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tempis_timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TempisTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#canvas&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="cm"&gt;/* same options */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Past the basics, the stuff that's saved me time: dependencies between items with the connector arrows routed automatically (and it doesn't fall over in RTL layouts), bands for drawing reference markers and lines across the whole thing, per-item styling when you need one bar to stand out, and animated &lt;code&gt;focus()&lt;/code&gt; to glide over to a specific item or date instead of teleporting. You can export the current view to a PNG if someone wants a screenshot for a deck. And if you've already standardized on Luxon or Day.js, there's a date adapter so you don't have to fight it over date handling.&lt;/p&gt;

&lt;p&gt;The interactivity comes through callbacks, nothing exotic:&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;new&lt;/span&gt; &lt;span class="nc"&gt;TempisTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onItemClick&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;openDetails&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;onItemContextClick&lt;/span&gt;&lt;span class="p"&gt;:&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;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showMenu&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;pos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;onRangeChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchVisibleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&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 last one is quietly the useful one. &lt;code&gt;onRangeChange&lt;/code&gt; fires as the visible window moves, so you can lazy-load and only pull the data that's actually in view instead of shipping everything up front. For a live feed or a big historical dataset that's the difference between snappy and not.&lt;/p&gt;

&lt;p&gt;There's a React wrapper too, &lt;code&gt;@tempis/react&lt;/code&gt;, if you'd rather not manage the instance lifecycle yourself:&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;TempisTimeline&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;@tempis/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;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TempisTimeline&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;options&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="na"&gt;responsive&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="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onItemClick&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="nx"&gt;id&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clicked&lt;/span&gt;&lt;span class="dl"&gt;'&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="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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;&lt;code&gt;items&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt;, &lt;code&gt;bands&lt;/code&gt;, and &lt;code&gt;dependencies&lt;/code&gt; are props that sync on their own. When you need to do something imperative, like focus an item, grab the current range, or export an image, you reach for a ref:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TempisTimelineRef&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;animate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's all TypeScript with real declarations, and the React package re-exports the core types so you're not installing two things just to import &lt;code&gt;TempisTimelineItem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One bit of honesty before you &lt;code&gt;npm install&lt;/code&gt;: Tempis is source-available, not MIT. It's free for personal projects, learning, and evaluation, but commercial use that makes money needs a paid licence. Worth a look at the LICENSE so there are no surprises later.&lt;/p&gt;

&lt;p&gt;If you want to poke at it before writing any code, the &lt;a href="https://tempis.dev/examples.html" rel="noopener noreferrer"&gt;examples page&lt;/a&gt; has live demos, and the &lt;a href="https://tempis.dev/api/core/getting-started.html" rel="noopener noreferrer"&gt;getting started docs&lt;/a&gt; go deeper than this post does. If you end up building something with it, I'd genuinely like to hear what. The timeline UIs people come up with are always more interesting than the demos.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I tried to use vis.js… then SSR and performance got in the way</title>
      <dc:creator>Nikolas Howard</dc:creator>
      <pubDate>Fri, 05 Jun 2026 12:29:12 +0000</pubDate>
      <link>https://dev.to/nik_f289a9240080d43eb7ee9/i-tried-to-use-visjs-then-ssr-and-performance-got-in-the-way-1mec</link>
      <guid>https://dev.to/nik_f289a9240080d43eb7ee9/i-tried-to-use-visjs-then-ssr-and-performance-got-in-the-way-1mec</guid>
      <description>&lt;p&gt;I needed a timeline for a reporting feature.&lt;/p&gt;

&lt;p&gt;Not just something interactive in the browser, but something that could also be rendered server-side and exported (PDFs, images, etc).&lt;/p&gt;

&lt;p&gt;It also needed to show &lt;strong&gt;dependencies between items&lt;/strong&gt;, not just a flat set of events.&lt;/p&gt;

&lt;p&gt;Seemed straightforward enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Started with vis.js
&lt;/h2&gt;

&lt;p&gt;vis.js was a pretty solid choice.&lt;/p&gt;

&lt;p&gt;It’s been around forever, lots of features, decent docs. I figured it’d cover everything.&lt;/p&gt;

&lt;p&gt;And to be fair it worked fine at first.&lt;/p&gt;

&lt;p&gt;Until the requirements stopped being “demo-sized”.&lt;/p&gt;




&lt;h2&gt;
  
  
  Requirement #1: server-side rendering
&lt;/h2&gt;

&lt;p&gt;This is where things immediately got messy.&lt;/p&gt;

&lt;p&gt;The timeline needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;render on the server&lt;/li&gt;
&lt;li&gt;be consistent (same output every time)&lt;/li&gt;
&lt;li&gt;plug into a reporting pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;vis.js is very DOM/browser-driven, so SSR was basically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not supported unless we also wanted to also have to deal with a headless browser&lt;/li&gt;
&lt;li&gt;possible with hacks I didn’t want to maintain long-term
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That alone was a pretty big red flag.&lt;/p&gt;




&lt;h2&gt;
  
  
  Requirement #2: dependencies between items
&lt;/h2&gt;

&lt;p&gt;This one turned out to be just as awkward.&lt;/p&gt;

&lt;p&gt;The timeline wasn’t just visualising events, it needed to show relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;item A blocks item B
&lt;/li&gt;
&lt;li&gt;task C depends on task D finishing
&lt;/li&gt;
&lt;li&gt;chains of related work across time
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, that means &lt;strong&gt;links between items, not just items on a timeline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;vis.js doesn’t really handle this cleanly out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no first-class concept of dependencies
&lt;/li&gt;
&lt;li&gt;you end up hacking it together yourself
&lt;/li&gt;
&lt;li&gt;quickly gets messy when the data isn't trivial
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Then the data got bigger
&lt;/h2&gt;

&lt;p&gt;Once I started feeding in more realistic data, things slowed down fast.&lt;/p&gt;

&lt;p&gt;Nothing crazy either, just enough items to resemble a real product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scrolling started to feel heavy&lt;/li&gt;
&lt;li&gt;zooming wasn’t smooth&lt;/li&gt;
&lt;li&gt;lots of DOM nodes getting created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add dependencies on top of that, and things got even harder to reason about.&lt;/p&gt;

&lt;p&gt;It just didn’t feel like something I’d want in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  React didn’t help much either
&lt;/h2&gt;

&lt;p&gt;Trying to wrap vis.js cleanly in React felt… off.&lt;/p&gt;

&lt;p&gt;Lots of imperative code, lifecycle juggling, things getting out of sync.&lt;/p&gt;

&lt;p&gt;It worked, but it never felt like it &lt;em&gt;fit&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  At that point it was easier to start over
&lt;/h2&gt;

&lt;p&gt;I didn’t originally plan to build a timeline library, but it got to the point where working around everything was more effort than just doing it properly.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Tempis&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://tempis.dev" rel="noopener noreferrer"&gt;https://tempis.dev&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The main decision: stop relying on the DOM
&lt;/h2&gt;

&lt;p&gt;Most of the issues came back to one thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;too much DOM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I switched to a canvas-based approach instead.&lt;/p&gt;

&lt;p&gt;That ended up solving a lot of problems at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;far fewer rendering bottlenecks
&lt;/li&gt;
&lt;li&gt;smoother interactions
&lt;/li&gt;
&lt;li&gt;more predictable output (important for reports)
&lt;/li&gt;
&lt;li&gt;a cleaner way to render relationships between items&lt;/li&gt;
&lt;li&gt;it's super easy to render a canvas to an image &lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What it’s actually aimed at
&lt;/h2&gt;

&lt;p&gt;This isn’t really a “toy timeline” library.&lt;/p&gt;

&lt;p&gt;It’s for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reporting views
&lt;/li&gt;
&lt;li&gt;product roadmaps
&lt;/li&gt;
&lt;li&gt;activity timelines
&lt;/li&gt;
&lt;li&gt;scheduling / Gantt-style interfaces
&lt;/li&gt;
&lt;li&gt;anything where you have &lt;em&gt;a lot&lt;/em&gt; of time-based data
&lt;/li&gt;
&lt;li&gt;and especially where items are &lt;strong&gt;connected, not just listed&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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