Today we are introducing the web counterpart to our command-line domain acquisition feature.
Just head to /domains to find your perfect domain (or use zeit.domains as a shortcut).
ZEIT Domains is powered by Now serverless deployments, seamlessly scaled to North America and Europe. It makes use of Next.js combined with some React Fiber features to bring you results with as little latency as possible.
Read on to learn not just how this feature works, but also how it was built.

How it Works

We redesigned the /domains page to present you with just an input.

Our new `domains` UI is simply focused on search

Results take up the full screen real estate of your device, something we think you will definitely appreciate with the large (and growing) variety of gTLDs.
There are no shopping carts. You can buy your names as instantly as you can now domains buy <domain> or make an API call, with just one click or tap.
Once your domain is purchased, we offer free domains privacy and free worldwide DNS. Point your domain anywhere or deploy and alias.

Genesis

At ZEIT, domain name search and acquisition is powered by several microservices.
One important observation about these services is that they exhibit very diverse latency characteristics.
For example, some registrars simply take longer to respond to queries, or throttle queries more aggressively. This translates to a longer wait to know a domains availability.
We also knew we wanted the user’s search to feel instant and effortless. As the user types, we want to start dispatching queries to get the data from our services as soon as possible, and similarly render them as soon as they become available to the user. With this in mind, we would have to embrace asynchrony at every level of the stack.

Server Rendering

The frontend is powered by Next.js, our React toolkit that enables seamless pre-rendering of the different entry points of an application.
If you go directly to our domains search tool, you’ll be met with an input immediately. No need to wait for JavaScript to load, code to be parsed, compiled and evaluated.
If you type and press enter, we’ll handle the <form> submission to our results page.
If the page logic loads in time for a user event, we perform an instant transition to the results page.

The input screen transforms into the results layout as the user types. The resulting URL is shareable.

Something to notice is that the UI transitions are driven by changes in the URL. As the user types, we populate ?q=$domain in the URL. This retains important functionality that has to do with:
  • Traversing forwards and backwards through the browser history
  • Sharing URLs, containing results, with others
  • Refreshing the page and retaining the current state
The last two items are particularly interesting with regards to server-side rendering.
Since we know the set of existing TLDs ahead of time, we can always pre-render the results optimistically, in a waiting state. That includes domain hacks, like ne.at.

Streaming Search Results

As mentioned earlier, we are operating under the assumption that the different API endpoints dedicated to domain name availability and price can respond at different times.
In addition, as the user scrolls the page, the subset of the results they are interested in varies.
For example, consider when the user presses ⌘ + F and jumps to a TLD of their interest.

All the browser features work as expected, without tradeoffs in performance.

We designed a protocol on top of WebSocket that has the following properties:
  • The client can communicate the search term, together with all the TLDs it’s interested in
    { "type": ”search”, "tlds": ["com", "co"], "q": "woot" }
    • As the user scrolls or types, we can re-send this action.
    • If the connection is lost, we restart it and re-emit this action.
    • The set of TLDs can include the previous and next “pages” in anticipation for user movement.
  • The server responds with Redux-style action objects by using a newline-delimited JSON stream
    • If a domain is available, it emits { “type”: “status”, “domain”: “javive.life“ }
    • If a price arrives later from a different backend, it can similarly be emitted independently with { "type": "price" }
    • This same mechanism can be used to communicate errors and timeouts
We take advantage of the statefulness of the WebSocket connection to maximize performance.
For example, if the user changes the search term completely, we emit a new search action. The server keeps state associated with the connection of what searches were already ongoing. It performs a diff of the ongoing searches, retains the ones the user is still interested in, and all the rest are aborted.
Persistent connections require being more careful about reconnection and timeouts. We tested a variety of scenarios like slow connections and interruptions to the WebSocket stream to ensure it worked smoothly under less than ideal conditions.

Session Cache

Domain availability can change fairly quickly. A domain name you are interested in could be snatched by someone else at a moment’s notice.
In fact, by the time we get the availability results back, they could already be stale!
We embrace this risk of “stale reads” by assuming that by the time the purchase is made, we need to contemplate for a state where the domain is already gone.
As a precaution, we also ensure that the price we quoted is double-checked by the time the purchase request is sent.
A typical user behavior is to type in quickly and iterate through many different ideas. Some queries might, in fact, be made multiple times.
To optimize that case, we maintain a client-side cache of recent availability checks, and we avoid sending them to the server more than once.

Asynchronous Rendering

One of the cornerstones of good GUI performance is to never block user input.
Imagine typing through domain names, changing your mind, but being blocked by how long it’s taking to render the list of domains you are no longer interested in!
Most web applications suffer from this problem. The main reason for this is that the user input is happening concurrently with the rendering of the UI on the same thread.
Even if we apply techniques like debouncing the input, once the timer fires and the search results start to render, subsequent user input can only happen as soon as rendering completes. This insight is behind the design of React 16 (codename “Fiber”), which introduces a new unstable_AsyncComponent interface we can use.
Making a top level component inherit from unstable_AsyncComponent instead of Component allows React to make updates in child components asynchronous which means the updates are treated as low priority. If you need to perform a synchronous update you can do it in a callback function passed to ReactDOM.flushSync(fn).
For example, this has a clear use case when the user is typing a query. We want to change the input value immediately so we use flushSync to update that piece of state. On the other hand, the search results are not needed right away so we perform a normal setState that will make the render optimum and asynchronous which has a huge impact providing a smoother UI.

Intersection Observers

In a similar vein as unrestricted keyboard input, a related goal was smooth scrolling that never blocks. Also, even with the mentioned performance enhancements, still there would be renders over elements that are completely out of the viewport and therefore are not needed at all. For this, we turned to the new Intersection Observer API. From MDN:
The Intersection Observer API allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element; for the purpose of this API, this is called the root element or root.
Since this API is imperative and DOM-focused, we decided to create an abstraction that worked better with React. This is what it looks like:

    <IObserver
      rootMargin="40% 0%"
      onIntersect={entry => {
        visibilityManager.updateItemVisibility(this.props.id, entry.isIntersecting, this.props.domain)
        if (entry.isIntersecting && this.isPendingUpdate) {
          this.forceUpdate()
        }
      }}
    >
      <button>...</button>
    </IObserver>
  
In this example, we are wrapping each element with an Intersection Observer so when it changes its visibility over the associated margin, the callback is going to be triggered. Therefore its visibility will be updated in a subscription module that keeps track of the visible elements. It can also do any other operation that should happen as it becomes visible like, for instance; running a pending update.

Conclusion

We really enjoyed building this product. It really demonstrates the simplicity of development and deployment that our tools enable.
As we followed a service-oriented architecture, any of the components that went into building this experience can be easily replaced later.
For example, using path alias we can iterate on /domains independently of the rest of the platform and even try out other technologies.
Now also allows us to experiment with different programming languages and concurrency models. Having had the flexibility to operate with WebSocket just as easily as HTTP/2 was a decisive advantage for this project that translates into lower waiting times for our customers.
If you want to build products like this, check out our Public API for domain name acquisition, Next.js for React development or get started deploying any static, Node.js or Docker project.
If you have any specific questions about this project and the methods we used to create it, don't hesitate to reach out to us on Slack.