Two years of Next.js at Artsy: A Retrospective

By Christopher Pappas

Where to begin! Where to begin…

Lets start with a good measure of success, one that I think most engineers could agree on: a from-scratch rebuild of a complex, internal tools app. Success with rebuilds is often fleeting, as many can attest. There’s risk involved. Extending the challenge further, lets assign (most) of the task to a team of platform engineers who aren’t too familiar with modern front-end technology – and then say that we succeeded.

Is Artsy crazy? Or is Next.js (along with our team 🙂) just that good? Well, it’s a little of both. And it’s a little bittersweet, in a way. Here’s our tale.

Beginnings

Our first formal use of Next came from a project spun up during Hackathon, two years ago. Hackathon’s are great opportunities to push technology forward, and Artsy has had a lot of success with them. We’d been talking about Next.js for a while, and experimenting here and there, but we were never able to find the proper intersection of product and purpose to take things further. With this in mind, one of our Engineers (Roop 👋) spun up a Next.js POC that allowed us to deduplicate artist records. On the surface, the effort was non-trivial. It involved authentication, DB communication, and more. Yet when presented during Hackathon, all of this was done and it also looked beautiful.

How could so much product work get completed in such a short period of time? A large part of that is due to Next.js’s framework design, and how it simply gets out of the way and allows one to start building product features. The defaults made sense; to create a new route, simply create a new folder or file in the pages directory, and Next takes care of the rest. And what about compilation, TypeScript, linting, and all of the other extraordinarily confusing JavaScript toolchain details that folks typically struggle over? (Most) of the setup was taken care of at the framework level, and the API side of things was light and easy to understand, thanks to great documentation.

There are omitted details of course, and there were things we needed to figure out (such as auth, via next-auth, and passing runtime ENV secrets to our Dockerized container), but in any case this gave us a lot of confidence to start discussing what it might take to seriously consider rebuilding our internal tools app. A few meetings later it was decided, and our platform team agreed to take it (with loose backup support from a couple other engineers). And a few short month’s later much of the core functionality was complete, executed by a team working amidst unfamiliar terrain.

That’s the definition of success, and the measure. With our internal tools app complete it was natural to start looking around for other opportunities – but first, a quick detour.

Enter: Next 13 (aka, The app Router)

Many of the limitations of Next 12 are well known, with the most notable ones being the inability to (naturally) do page layouts along with quirks around getServerSideProps and _app and _document request fetching not being as flexible as one would like. These issues, however, were not deal-breakers; and in fact, we hardly even noticed them. Our page layouts simply rendered a shared <Layout> component that accepted children; and per-route data fetching requested data per-route, with globally shared data going in _app. It got the job done.

With these things in mind (and a few things more), many of us were extremely excited when Next released its Layouts RFC, outlining the next version of Next.js, which would be built on top of another long-anticipated React.js feature, React Server Components.

In short, the Layouts RFC outlined what looked to be a beautifully obvious, yet performant architecture. Using Next’s preference for file-system based configuration, a typical “page” could soon look like this:

- app/
---- layout.tsx
---- page.tsx
- app/artist
---- layout.tsx
---- page.tsx
---- middleware.tsx

And so on. “Global” SSR data could be fetched right there in layout or page and shared with its subtree; and likewise, for sub-sub-tree’s we could do the same in each individual app layout or page component.

Additionally, with React Server Components, we would no longer need to use many Next.js-specific APIs. To fetch data on the server, you simply await a promise and pass it right to your component as props. Next’s already minimal API footprint would diminish even further, and it became possible to glean a vision of “just vanilla JS” all the way down, and within that vision the possibility of true simplicity.

Little did we know, it wouldn’t take much to turn this beautiful vision into something of a dilemma, but that part of the story comes a bit later.

Expanding Next.js at Artsy

Coming off of our success with the internal tools app rebuild, we wanted more. And we didn’t need to look far: right around the corner was a ~10 year old external CMS app that our partners use to manage their inventory.

We decided the path forward was Next, and like our internal tools app rebuild, for the most part it has been a success. We again went with the pages router (as the new app router wasn’t yet released) and so far there’s been minimal confusion from the team. And buisness-wise, its been refreshing to defer framework design decisions, lib upgrades and more to Next, versus having to maintain all of these things in-house.

It’s also worth mentioning that there have been a few significant challenges involved (such as setting up performant SSR patterns for using Relay, our GraphQL client – thats another blog post), but on the whole Next has served our needs well. Team performance was unlocked, and we’ve been able to quickly get to building and rebuilding CMS pages in this new application. And our engineers have loved working in it.

Back to Next 13

In the meantime, Next 13 was released. Imagine our excitement! Just as this new app is spinning up we receive a little gift from the stars. Carlos and I are the first ones to bite; lets see what migrating our work over to the new framework might look like, what kind of effort.

From the start, it was immediately obvious that the Next.js team released an alpha-quality (or less) product, marked as stable. Not a beta, not an RC to peruse and experiment with, but rather an

npm install next

package that comes with an application scaffold generator that suggests using the app router over pages – marked as “Recommended”. In other words, highly polished. And what’s the first thing one experiences? Hot Reloading is broken.

And what’s the next thing? Styles are broken. It turns out that RSC (React Server Components) doesn’t fully support pre-existing CSS-in-JS patterns. Or rather, they do, kind of, but they can only be used inside use client components (which, in Next.js, actually means a server-side rendered component environment that’s separate from a RSC rendered “server-only” environment – aka the old pages router model). And we certainly weren’t about to throw out our Design System component library Palette, which has been nothing but a runaway success (and a highly portable one at that).

With this limitation, our ability to use React Server Components had been severely hampered. Excluding the root-most level of our component tree, we were now required to prepend use client on the top of every component, lest we receive ambiguous errors about rendering a client component (which used to be server-side render safe) on the “RSC server”.

Things can be taught, however. So lets proceed from the assumption that through some kind of tooling / linting layer, use client is added to every new component. It should behave at that point just like the old Next and now we get the best of both worlds. Nope: turns out that even with the CSS-in-JS setup instructions described in the the next docs above, we still run into issues. There are bugs.

(These are the two main red flags, but there are many others as well.)

At this point, we wisely back out. It’s only next@13.0.0, and what they’re doing here is to a certain extent revolutionary. It’s a new way of thinking about React, yet an old way of thinking about page rendering. It’s like… PHP, or so they say. RSC is interesting, there’s something to it. Lets give them the benefit of the doubt and return to things in a few months, after a few minor version bumps; there are, after all, countless eyes on the project.

Many Months Later

We run npx create-next-app@latest (this is around the time they release 13.4) and then add these two components inside the newly-created vanilla project:

// app/HelloClient.tsx
'use client'

export const HelloClient = () => {
  return <div>Does this hot reload</div>
}

// app/layout.tsx
import { HelloClient } from './HelloClient'

export default Layout() {
  return <HelloClient />
}

Everything renders. And then

export const HelloClient = () => {
  return <div>Does this hot reload... nope :(</div>
}

In the most basic project setup, the most obvious Next.js selling point – Developer Experience – failed to deliver. Vercel is really forcing us to question things. But we’re flexible, and we like to investigate at Artsy, so even though this definitely-required feature doesn’t quite work, maybe it will once we’re done with our migration spike, and maybe we can still take advantage of everything else that RSC has to offer.

So again, we start refactoring the project. Stuff from the pages directory starts getting copied over to app. We update configuration. We setup styling (it seems to work better). Things are almost there. But then the obscure framework errors start to arrive, and CSS still doesn’t quite work: it turns out that refactoring across RSC-use client boundaries is harder than one thought. I.e., if any piece of “client” (remember, ‘use client’ actually means SSR-safe) code anywhere in the dependency tree happens to intersect an RSC boundary, the whole thing will fail. And this includes any use of React’s createContext – because React Contexts aren’t supported. Given an app of any reasonable size, you’re likely to rely on a context somewhere, as contexts are so critical within the react hooks model of behavior. Said contexts might come from within your app, and if not there they’ll certainly come from a 3rd party library.

One would expect the errors to be helpful in tracking this down – Next.js is all about DX – but no. Confusion reigns.

We’re experts though, and we eventually do find the source of the violation, and we make sure to create a “safety wrapper” around the offender so that it doesn’t happen again. But it does happens again – and again, any time any piece of any complexity is added in the new RSC-intersected route. It’s rather unavoidable. And each time solvable, but at a great cost to the developer. Thankfully we know what we’re doing!

Another trivial yet annoying issue (thankfully fixed with some custom eslint config) is accidentally importing the useRouter hook from the app router location, or redirect, or any number of other new app router features, because all of these things don’t work in pages, and will error out. The errors here are slightly less opaque, but what if you’re a backend dev who knows nothing about any of this? Googling “useRouter next” now yields two sets of docs. Figure it out.

At this point, we make a judgement call: this simply isn’t going to work at Artsy. We’re here to empower folks and unlock productivity. Remember the team of DevOps engineers on Platform who rebuilt a CMS in record time? In the new Next 13 model, that would be unfathomable, impossible even. Paper cuts would kill motivation, and dishearten the already skeptical. And the front end already has a bad rap, for good reason: historically, everything that seems like it should be easy is hard and confusing for those who aren’t experts. And everything is always changing. And the tooling is always breaking. And everybody always has a bright new idea, one that will finally end this madness for good.

A certain amount of sadness is appropriate here, because Next’s pages router was very, very close to being the silver bullet for web applications that we’ve all been looking for. Even though the pages router has its flaws, it showed us that it’s possible to get something out the door very quickly with little prior knowledge of Front End development. This is no small thing. Next 13’s fatal error is that its destiny, being coupled to RSC, now requires experts. And by ‘expert’ I mean: those with many years of experience dealing with JavaScript’s whims, its complex eco-system, its changeover, as well as its problems. In short, folks who have become numb to it all. This is no way to work. Thankfully the community is finally responding.

A Quick Note on Performance

It’s worth remembering that Next 12 was industry-leading in terms of performance and pioneered many innovative solutions. Let me say it again: Next’s pages router IS fast. Next 13 combined with RSC is faster, but at what point does an obsession with performance start negating other crucial factors? What’s good for the 90%? And what’s required for the other 10%? Most companies just need something that’s fast enough – and easy enough – to move fast. And not much more.

Back At Artsy…

With all of this in mind, and with the uncertainty around long-term support for Next.js pages (amongst other things), we recently decided to hit pause on future development in our new external CMS app rebuild. Weighing a few different factors (many entirely unrelated to Next), including a team reorg that allowed us to look more closely (and fix) the DX in our old external CMS app, we took a step back and recognized that our needs are actually quite minimal:

  • Instant hot reloading
  • Fast, SPA-like UX performance
  • Simple, convention-based file organization

With these things covered, the “web framework” layer looks something like the following, minus a few underlying router lib details:


const Router = createRouter({
  history: 'browser',
  routes: [
    {
      path: '/foo',
      getComponent: React.lazy(() => import('./Foo'));
    },
    {
      path: '/bar',
      getComponent: React.lazy(() => import('./Bar'));
    }
  ]
})

We’re now required to manage our compiler config, but that layer isn’t too complicated once its setup, and it works great. (If you’re using something like Vite, it could be even simpler.)

Final Thoughts

Next 13 and React Server Components are very intriguing; it’s a new model of thinking that folks are still trying to work out. Like other revolutionary technologies released by Meta, sometimes it takes a few years to catch on, and maybe RSC is firmly in that bucket. Vercel, however, would do well to remember Next’s original fundamental insight – that all good things follow from developer experience. Improvements there tend to improve things everywhere.

In addition to fixing some of the obvious rough edges in the new app router, it would be helpful if Next could provide an official response to the question of long-term Pages support. There’s been quite a backlash in the community against Next 13, and that should give all developers pause. It’d also be great to get word on whether there will be any further development on the pages router – perhaps some of the features from the new app router can be migrated to pages as well? – or if the pages router is officially deprecated and locked. All of this is currently ambiguous.

Another area where Next could improve is their willingness to ship buggy features, and to rely on patched versions of React in order to achieve certain ends. Even though Vercel employs many members of the React Core team, by releasing Next versions that rely on patched and augmented canary builds of React, Vercel is effectively compromising some of React’s integrity, and forcing their hand. Once a neo-React feature is added to Next, it makes it hard to say no; Next has captured too much of the market-share.

All of this calls for sobriety and hesitation on the part of developers working with – and building companies on top of – Vercel’s products. Next is Open Source, yes, but it’s also a wildcard. Artsy has had some real success with Next, but sometimes that’s just not enough to avoid hitting pause, and looking at the bigger picture. Inclusivity should always win.