<?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: Angus Russell</title>
    <description>The latest articles on DEV Community by Angus Russell (@angus_russell).</description>
    <link>https://dev.to/angus_russell</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F333156%2F213a12aa-d4ba-4aed-b9f0-6e08b3530361.jpg</url>
      <title>DEV Community: Angus Russell</title>
      <link>https://dev.to/angus_russell</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/angus_russell"/>
    <language>en</language>
    <item>
      <title>Super Simple List Virtualization in React with IntersectionObserver</title>
      <dc:creator>Angus Russell</dc:creator>
      <pubDate>Wed, 12 May 2021 04:27:47 +0000</pubDate>
      <link>https://dev.to/angus_russell/super-simple-list-virtualization-in-react-with-intersectionobserver-3o6g</link>
      <guid>https://dev.to/angus_russell/super-simple-list-virtualization-in-react-with-intersectionobserver-3o6g</guid>
      <description>&lt;p&gt;&lt;em&gt;Want smoother scrolling, but having trouble getting react-virtualized or react-window to work in your app? Try this dead-simple drop-in virtualization technique instead.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some quick background
&lt;/h2&gt;

&lt;p&gt;I run a popular &lt;a href="https://creator.nightcafe.studio" rel="noopener noreferrer"&gt;AI Art Generator App&lt;/a&gt; that's built on React. A big part of the user experience is simply scrolling through the feed of AI generated art that other users - or you - have created using the app. I personally use a fairly low-end Oppo smartphone and I noticed that the more artworks I scrolled through, the more jittery the scroll became. That's because as more artworks are loaded (via infinite scroll), React is struggling to render them all at once in - or even close to - 17 milliseconds (60 frames per second).&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard solutions
&lt;/h2&gt;

&lt;p&gt;So what can be done about this? The seasoned React dev knows that this is a problem that requires virtualization.&lt;/p&gt;

&lt;p&gt;But what is virtualization? Essentially it means only rendering the list items that are on - or near - the viewport. In other words - only render the visible items and skip the rest.&lt;/p&gt;

&lt;p&gt;Virtualisation is simple in theory, but a bit harder in practice. There are two commonly used React libraries for implementing virtualization - &lt;a href="https://github.com/bvaughn/react-window" rel="noopener noreferrer"&gt;react-window&lt;/a&gt; and &lt;a href="https://github.com/bvaughn/react-virtualized" rel="noopener noreferrer"&gt;react-virtualized&lt;/a&gt;. &lt;em&gt;Both&lt;/em&gt; of these libraries are maintained by &lt;a href="https://github.com/bvaughn" rel="noopener noreferrer"&gt;Brian Vaughn&lt;/a&gt;, who is also a member of the core React team at Facebook.&lt;/p&gt;

&lt;p&gt;As an experienced React developer, I've dealt with this problem in the past, and I already knew about these two libraries. I also knew that while they &lt;em&gt;are&lt;/em&gt; great libraries, they are actually quite difficult to implement in many situations - particularly when your list items are of varying sizes, not in a 'flat' list, responsive height, in a responsive grid, or have other elements interspersed (E.g. advertisements).&lt;/p&gt;

&lt;p&gt;I did spend a while trying to get react-virtualized (the more flexible of the two) working on my list items, but after a couple of hours of roadblocks, I wondered if there was an easier, simpler solution to my problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter &lt;code&gt;IntersectionObserver&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" rel="noopener noreferrer"&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;&lt;/a&gt; is a browser API - available on all modern browsers - that provides a way to execute a callback when a HTML element intersects with a parent element, or the browser viewport itself. Put more simply, it can tell us when our list items are on (or near) the screen as the user scrolls down the page.&lt;/p&gt;

&lt;p&gt;I knew about Intersection Observers, having previously used them as a way to lazy-load images (before &lt;code&gt;&amp;lt;img loading="lazy" /&amp;gt;&lt;/code&gt; was a thing). Something made me think of this API while I was having virtualization woes, so I decided to see if it could solve my problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The joy of simple lazy rendering
&lt;/h2&gt;

&lt;p&gt;It took a little while to read through the &lt;code&gt;IntersectionObserver&lt;/code&gt; spec and think about how I could React-ify it in a way that would suit my lazy-rendering use-case, but surprisingly, I encountered very few issues and quickly ended up with a super simple React component that I called &lt;code&gt;&amp;lt;RenderIfVisible /&amp;gt;&lt;/code&gt; which I could simply wrap around my list items at &lt;em&gt;any depth&lt;/em&gt; (no need for a flat list), to defer rendering until the item is near the viewport, then go back to rendering a plain div when the item leaves the viewport.&lt;/p&gt;

&lt;p&gt;While it does have a couple of drawbacks, which I'll list a bit later, it comes with these advantages over react-virtualized or react-window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need for a flat list&lt;/li&gt;
&lt;li&gt;Works with any DOM nesting structure&lt;/li&gt;
&lt;li&gt;Is completely decoupled from infinite-scroll or pagination&lt;/li&gt;
&lt;li&gt;Works for responsive grids with no extra configuration&lt;/li&gt;
&lt;li&gt;Easy to drop in - just wrap your list items with &lt;code&gt;&amp;lt;RenderIfVisible&amp;gt;&amp;lt;/RenderIfVisible&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Doesn't require a wrapper around your entire list&lt;/li&gt;
&lt;li&gt;Doesn't care how scrolling works for your situation (i.e. is it window scroll, or scrolling within a div with &lt;code&gt;overflow: scroll&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;It is &lt;em&gt;tiny&lt;/em&gt; - &lt;strong&gt;46 lines&lt;/strong&gt; and has &lt;em&gt;no dependencies&lt;/em&gt; (apart from React as a peer dependency).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where can I get it?
&lt;/h2&gt;

&lt;p&gt;On Github...&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/NightCafeStudio" rel="noopener noreferrer"&gt;
        NightCafeStudio
      &lt;/a&gt; / &lt;a href="https://github.com/NightCafeStudio/react-render-if-visible" rel="noopener noreferrer"&gt;
        react-render-if-visible
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Harness the power of Intersection Observers for simple list virtualization in React
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or &lt;a href="https://www.npmjs.com/package/react-render-if-visible" rel="noopener noreferrer"&gt;install it via npm&lt;/a&gt;...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;react-render-if-visible &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or yarn.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add react-render-if-visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Show me under the hood
&lt;/h2&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;defaultHeight&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;visibleOffset&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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;RenderIfVisible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;defaultHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;visibleOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&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;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isServer&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;placeholderHeight&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="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaultHeight&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;intersectionRef&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;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Set visibility with intersection observer&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&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;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;entries&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestIdleCallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestIdleCallback&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="nf"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&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="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="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;visibleOffset&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px 0px &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;visibleOffset&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px 0px`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return &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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&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="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unobserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&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="p"&gt;}&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;// Set height after render&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;placeholderHeight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intersectionRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&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="nx"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intersectionRef&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;intersectionRef&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="nx"&gt;isVisible&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;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;placeholderHeight&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;RenderIfVisible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yep, that's the whole thing! Let me describe the important parts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We pass a &lt;code&gt;defaultHeight&lt;/code&gt; prop which is an estimate of the element's height. This only used when the element is &lt;em&gt;not&lt;/em&gt; visible, and helps to avoid erratic scrollbar resizing.&lt;/li&gt;
&lt;li&gt;We also pass a &lt;code&gt;visibleOffset&lt;/code&gt; prop, which tells the component how far outside the viewport to start rendering. The default is 1000, which means elements will render when they're within 1000px of the viewport.&lt;/li&gt;
&lt;li&gt;We keep two pieces of state: &lt;code&gt;isVisible&lt;/code&gt;, which is used to trigger re-renders and render either the &lt;code&gt;{children}&lt;/code&gt; or the placeholder; and &lt;code&gt;placeholderHeight&lt;/code&gt; which we keep in a &lt;code&gt;ref&lt;/code&gt; (to avoid causing re-renders) - we keep the &lt;code&gt;defaultHeight&lt;/code&gt; here and update it with the actual calculated height when the element becomes visible.&lt;/li&gt;
&lt;li&gt;When the component renders for the first time, the component gets access to the wrapping element in the &lt;code&gt;intersectionRef&lt;/code&gt; ref. It then sets up an &lt;code&gt;IntersectionObserver&lt;/code&gt; to observe this element and toggle the &lt;code&gt;isVisible&lt;/code&gt; state when the observer's callback is fired. This is done in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback" rel="noopener noreferrer"&gt;&lt;code&gt;window.RequestIdleCallback&lt;/code&gt;&lt;/a&gt; (if possible) to avoid rendering off-screen (but within 1000px of the viewport) components when other important main thread work is being done.&lt;/li&gt;
&lt;li&gt;In the return from our &lt;code&gt;useEffect&lt;/code&gt;, we call &lt;code&gt;unobserve&lt;/code&gt; on the observer, because we are good citizens.&lt;/li&gt;
&lt;li&gt;We have another &lt;code&gt;useEffect&lt;/code&gt; that runs when &lt;code&gt;isVisible&lt;/code&gt; is toggled. If the component is visible, we update the &lt;code&gt;placeholderHeight&lt;/code&gt; ref with the calculated height of the visible element. This value is kept in a ref (rather than react state) so that it doesn't cause the component to re-render. When &lt;code&gt;isVisible&lt;/code&gt; is toggled back to false, the placeholder will use the calculated height.&lt;/li&gt;
&lt;li&gt;The component returns either the &lt;code&gt;{children}&lt;/code&gt; or the placeholder element depending on the value of &lt;code&gt;isVisible&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results from use in production
&lt;/h2&gt;

&lt;p&gt;I've been using this component throughout &lt;a href="https://creator.nightcafe.studio" rel="noopener noreferrer"&gt;NightCafe Creator&lt;/a&gt; for 9 months now (according to my commit history), and haven't noticed any scrolling jank or performance issues in that time. On screens where my Oppo smartphone used to struggle massively, I can now scroll smoothly through hundreds of artworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about those drawbacks?
&lt;/h2&gt;

&lt;p&gt;First, when I say drawbacks, I don't mean drawbacks compared to &lt;em&gt;no&lt;/em&gt; virtualization, I mean drawbacks compared with other virtualization libraries. I think these drawbacks are very minor, but I'm listing them here for you anyway.&lt;/p&gt;

&lt;p&gt;First, we end up with extra containing &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s in our markup. These are required for setting placeholder height and attaching the observer.&lt;/p&gt;

&lt;p&gt;Also, a new &lt;code&gt;IntersectionObserver&lt;/code&gt; is created for every element that you wrap in &lt;code&gt;&amp;lt;RenderIfVisible&amp;gt;&amp;lt;/RenderIfVisible&amp;gt;&lt;/code&gt;. This does result in some extra performance overhead - especially if there are hundreds or thousands of items. I can scroll through hundreds or thousands of items on my mid-tier smartphone without noticing any degradation, so this hasn't bothered me so far. However if you really need the absolute best performance of any solution, you might be better off using &lt;a href="https://github.com/bvaughn/react-window" rel="noopener noreferrer"&gt;react-window&lt;/a&gt; and spending some extra time to get it working with your setup.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt; offers a simple, native way to detect when HTML elements are on or near the viewport, and &lt;code&gt;&amp;lt;RenderIfVisible /&amp;gt;&lt;/code&gt; is a very simple and easy-to-implement component to harness that power to speed up the performance of long lists in your React app.&lt;/p&gt;

&lt;p&gt;I hope this component helps you get some quick performance wins. Questions or feedback? Let me know in the comments!&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Anatomy of an AI Art Generator App — how some smart technology choices helped me build and launch a complex app in under a month</title>
      <dc:creator>Angus Russell</dc:creator>
      <pubDate>Mon, 10 Feb 2020 09:48:47 +0000</pubDate>
      <link>https://dev.to/angus_russell/anatomy-of-an-ai-art-generator-app-how-some-smart-technology-choices-helped-me-build-and-launch-a-complex-app-in-under-a-month-408</link>
      <guid>https://dev.to/angus_russell/anatomy-of-an-ai-art-generator-app-how-some-smart-technology-choices-helped-me-build-and-launch-a-complex-app-in-under-a-month-408</guid>
      <description>&lt;p&gt;&lt;em&gt;Last year I started working on an AI Art Generator app called &lt;a href="https://creator.nightcafe.studio/"&gt;NightCafe Creator&lt;/a&gt;. I recently wrote an &lt;a href="https://medium.com/@angus.russell89/ai-art-generator-app-nightcafe-creator-54500fd4c782"&gt;article&lt;/a&gt; about how I conceived and then validated the concept. This article follows on from that one, and outlines the technology stack that I used to build it, and how my choices helped me build and launch the app in under a month.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article originally appeared on &lt;a href="https://medium.com/@angus.russell89/anatomy-of-an-ai-art-generator-app-13259e438ce5"&gt;Medium&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, a brief timeline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;October 14, 2019&lt;/strong&gt; — Looking back at my commit history, this is the day I switched focus from validating the idea of selling AI-generated artworks, to actually building the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--P7c4D8u_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/x4o9h7fl90gnm6dl3qeb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P7c4D8u_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/x4o9h7fl90gnm6dl3qeb.png" alt="Git history"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;October 28&lt;/strong&gt; — 2 weeks later I sent a Slack message to some friends showing them my progress, a completely un-styled, zero polish “app” (web page) that allowed them to upload an image, upload a style, queue a style-transfer job and view the result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xaiy5mvS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/a0y8akivz4yrvjqgwchb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xaiy5mvS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/a0y8akivz4yrvjqgwchb.png" alt="Grumpy cat"&gt;&lt;/a&gt;&lt;/p&gt;
My friend thought he’d be clever and try to style grumpy cat in the style of grumpy cat. Note the beautiful UI.



&lt;p&gt;&lt;strong&gt;October 30&lt;/strong&gt; — I sent another Slack message saying “It looks a lot better now” (I’d added styles and a bit of polish).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k_-aZv9D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4vxfwb93j7cxxqyis9gk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k_-aZv9D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4vxfwb93j7cxxqyis9gk.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;
Still no real url or even title or description tag



&lt;p&gt;&lt;strong&gt;November 13&lt;/strong&gt; — I posted it to Reddit for the first time. Launched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NxOSjYhc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/nirjzbjsou3x7re601mh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NxOSjYhc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/nirjzbjsou3x7re601mh.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;
Launch day — November 13, 2019



&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;A lot of functionality is required for an app like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPUs in the cloud to queue and run jobs on&lt;/li&gt;
&lt;li&gt;An API to create jobs on the GPUs&lt;/li&gt;
&lt;li&gt;A way for the client to be alerted of finished jobs and display them (E.g. websockets or polling)&lt;/li&gt;
&lt;li&gt;A database of style transfer jobs&lt;/li&gt;
&lt;li&gt;Authentication and user accounts so you can see your own creations&lt;/li&gt;
&lt;li&gt;Email and/or native notifications to alert the user that their job is finished (jobs run for 5+ minutes so the user has usually moved on)&lt;/li&gt;
&lt;li&gt;And of course all the usual things like UI, a way to deploy, etc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How did I achieve all this in under a month? It’s not that I’m a crazy-fast coder — I don’t even know Python, the language that the neural style transfer algorithm is built in — I put it down to a few guiding principles that led to some smart choices (and a few flukes).&lt;/p&gt;

&lt;h2&gt;
  
  
  Guiding Principles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No premature optimisation&lt;/li&gt;
&lt;li&gt;Choose the technologies that will be fastest to work with&lt;/li&gt;
&lt;li&gt;Build once for as many platforms as possible&lt;/li&gt;
&lt;li&gt;Play to my own strengths&lt;/li&gt;
&lt;li&gt;Absolute MVP (Minimum Viable Product) — do the bare minimum to get each feature ready for launch as soon as possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reasoning behind the first four principles can be summarised by the last one. The last principle — Absolute MVP — is derived from the lean startup principle of getting feedback as early as possible. It’s important to get feedback ASAP so you can learn whether you’re on the right track, you don’t waste time building the wrong features (features nobody wants), and you can start measuring your impact. I’ve also found it important for side-projects in particular, because they are so often abandoned before being released, but long after an MVP launch could have been done.&lt;/p&gt;

&lt;p&gt;Now that the stage has been set, let’s dive into what these “smart technology choices” were.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #1 — Queueing and running jobs on cloud GPUs
&lt;/h2&gt;

&lt;p&gt;I’m primarily a front-end engineer, so this is the challenge that worried me the most, and so it’s the one that I tackled first. The direction that a more experienced devops engineer would likely have taken is to set up a server (or multiple) with a GPU on an Amazon EC2 or Google Compute Engine instance and write an API and queueing system for it. I could foresee a few problems with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Being a front-end engineer, it would take me a long time to do all this&lt;/li&gt;
&lt;li&gt;I could still only run one job at a time (unless I set up auto-scaling and load balancing, which I know even less about)&lt;/li&gt;
&lt;li&gt;I don’t know enough devops to be confident in maintaining it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted instead was to have this all abstracted away for me — I wanted something like AWS Lambda (i.e. serverless functions) but with GPUs. Neither Google nor AWS provide such a service (at least at the time of writing), but with a bit of Googling I did find some options. I settled on a platform called &lt;a href="https://algorithmia.com/"&gt;Algorithmia&lt;/a&gt;. Here’s a quote from their home page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data scientists never have to worry about infrastructure again&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perfect! Algorithmia abstracts away the infrastructure, queueing, autoscaling, devops and API layer, leaving me to simply port the algorithm to the platform and be done! (I haven’t touched on it here, but I was simply using an open-source style-transfer implementation in tensorflow). Not really knowing Python, it still took me a while, but I estimate that I saved weeks or even months by offloading the hard parts to Algorithmia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #2 — The UI
&lt;/h2&gt;

&lt;p&gt;This is me. This is my jam. The UI was an easy choice, I just had to play to my strengths, so going with React was a no-brainer. I used &lt;a href="https://github.com/facebook/create-react-app"&gt;Create-React-App&lt;/a&gt; initially because it’s the fastest way to get off the ground.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;However&lt;/em&gt;, I also decided — against my guiding principles — to use TypeScript for the first time. The reason I made this choice was simply that I’d been noticing TypeScript show up in more and more job descriptions, blog posts and JS libraries, and realised I needed to learn it some time — why not right now? Adding TypeScript definitely slowed me down at times, and even at the time of launch — a month later — it was still slowing me down. Now though, a few months later, I’m glad I made this choice — not for speed and MVP reasons but purely for personal development. I now feel a bit less safe when working with plain JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #3 — A database of style-transfer jobs
&lt;/h2&gt;

&lt;p&gt;I’m much better with databases than with devops, but as a front-end engineer, they’re still not really my specialty. Similar to my search for a cloud GPU solution, I knew I needed an option that abstracts away the hard parts (setup, hosting, devops, etc). I also thought that the data was fairly well suited to NoSQL (jobs could just live under users). I’d used DynamoDB before, but even that had its issues (like an overly verbose API). I’d heard a lot about &lt;a href="https://firebase.google.com/"&gt;Firebase&lt;/a&gt; but never actually used it, so I watched a few videos. I was surprised to learn that not only was Firebase a good database option, it also had services like simple authentication, cloud functions (much like AWS Lambda), static site hosting, file storage, analytics and more. As it says on the Firebase website, firebase is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A comprehensive app development platform&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There were also plenty of React libraries and integration examples, which made the choice easy. I decided to go with Firebase for the database (Firestore more specifically), and also make use of the other services where necessary. It was super easy to setup — all through a GUI — and I had a database running in no time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #4 — Alerting the client when a job is complete
&lt;/h2&gt;

&lt;p&gt;This also sounded like a fairly difficult problem. A couple of traditional options that might have come to mind were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Polling the jobs database to look for a “completed” status&lt;/li&gt;
&lt;li&gt;Keeping a websocket open to the Algorithmia layer (this seemed like it would be very difficult)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn’t have to think about this one too much, because I realised — &lt;em&gt;after&lt;/em&gt; choosing Firestore for the database — that the problem was solved. Firestore is a realtime database that keeps a websocket open to the database server and pushes updates straight into your app. All I had to do was write to Firestore from my Algorithmia function when the job was finished, and the rest was handled automagically. What a win! This one was a bit of a fluke, but now that I’ve realised it’s power I’ll definitely keep this little trick in my repertoire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #5 — Authentication, Notifications and Deployment
&lt;/h2&gt;

&lt;p&gt;These also came as a bit of a fluke through my discovery of Firebase. Firebase makes authentication easy (especially with the readily available React libraries), and also has static site hosting (perfect for a Create-React-App build) and a notifications API. Without Firebase, rolling my own authentication would have taken at least a week using something like Passport.js, or a bit less with Auth0. With Firebase it took less than a day.&lt;/p&gt;

&lt;p&gt;Native notifications would have taken me even longer — in fact I wouldn’t have even thought about including native notifications in the MVP release if it hadn’t been for Firebase. It took longer than a day to get notifications working — they’re a bit of a complex beast — but still dramatically less time than rolling my own solution.&lt;/p&gt;

&lt;p&gt;For email notifications I created a Firebase function that listens to database updates — something Firebase functions can do out-of-the-box. If the update corresponds to a job being completed, I just use the SendGrid API to email the user.&lt;/p&gt;

&lt;p&gt;Creating an email template is always a pain, but I found the &lt;a href="https://beefree.io/"&gt;BEE Free&lt;/a&gt; HTML email creator and used it to export a template and convert it into a &lt;a href="https://sendgrid.com/docs/API_Reference/Web_API_v3/Transactional_Templates/index.html"&gt;SendGrid Transactional Email Template&lt;/a&gt; (the BEE Free template creator is miles better than SendGrid’s).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0X0au6L_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/9sqbxecek47ohdl9ry6q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0X0au6L_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/9sqbxecek47ohdl9ry6q.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;
The email template



&lt;p&gt;Finally, Firebase static site hosting made deployment a breeze. I could deploy from the command line via the Firebase CLI using a command as simple as&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build &amp;amp;&amp;amp; firebase deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which of course I turned into an even simpler script&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A few things I learned
&lt;/h2&gt;

&lt;p&gt;The speed and success of this project really reinforced my belief in the guiding principles I followed. By doing each thing in the fastest, easiest way I was able to build and release a complex project in under a month. By releasing so soon I was able to get plenty of user feedback and adjust my roadmap accordingly. I’ve even made a few sales!&lt;/p&gt;

&lt;p&gt;Another thing I learned is that Firebase is awesome. I’ll definitely be using it for future side-projects (though I hope that NightCafe Creator is successful enough to remain my only side-project for a while).&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I’ve changed or added since launching
&lt;/h2&gt;

&lt;p&gt;Of course, doing everything the easiest/fastest way means you might need to replace a few pieces down the track. That’s expected, and it’s fine. It is important to consider how hard a piece might be to replace later — and the likelihood that it will become necessary — while making your decisions.&lt;/p&gt;

&lt;p&gt;One big thing I’ve changed since launching is swapping the front-end from Create React App to Next.js, and hosting to Zeit Now. I knew that Create React App is not well suited to server-side rendering for SEO, but I’d been thinking I could just build a static home page for search engines. I later realised that server-side rendering was going to be important for getting link previews when sharing to Facebook and other apps that use Open Graph tags. I honestly hadn’t considered the Open Graph aspect of SEO before choosing CRA, and Next.js would have probably been a better choice from the start. Oh well, I will remember to consider that next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Please try my app
&lt;/h2&gt;

&lt;p&gt;I am ending this post with a plea — please try my app! &lt;a href="https://creator.nightcafe.studio/"&gt;NightCafe Creator&lt;/a&gt; is a Progressive Web App, so you just need to click the link on any device to use it. I’d love to see what kind of interesting art you can generate. Post one of your creations in the comments!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pcuaE_BM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/yi2i8bpwy9itmr9scobj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pcuaE_BM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/yi2i8bpwy9itmr9scobj.jpg" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;a href="https://creator.nightcafe.studio"&gt;NightCafe Creator&lt;/a&gt;



&lt;p&gt;Also keep in mind that what you see now is NOT what was launched on November 13. I’ve been constantly improving the app for the last 3 months since launch (based on valuable user feedback of course), and it’s a lot more polished now than it was on launch day.&lt;/p&gt;

&lt;p&gt;Of course if you’ve got any feedback on the app, please reach out to me at nightcafestudio at gmail dot com. If you’ve got feedback or comments on this article, post them below along with your creations.&lt;/p&gt;

&lt;p&gt;If you want to stay up to date with my journey, and news about NightCafe, please &lt;a href="https://gmail.us20.list-manage.com/subscribe?u=dd8bd7fe92d6acd8cc4645c12&amp;amp;id=c07748c383"&gt;subscribe to the newsletter&lt;/a&gt; and follow me on &lt;a href="https://medium.com/@angus.russell89"&gt;Medium&lt;/a&gt;, &lt;a href="https://twitter.com/angus_russell"&gt;Twitter&lt;/a&gt; and &lt;a href="https://www.reddit.com/user/gusruss89"&gt;Reddit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>javascript</category>
      <category>machinelearning</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
