wincent / masochist

⛓ Website infrastructure for over-engineers
MIT License
78 stars 26 forks source link

Replace Relay with minimal GraphQL client #180

Open wincent opened 2 years ago

wincent commented 2 years ago

Milestones

Done so far...

... next steps aren't as clear... perhaps:

The context

This supersedes https://github.com/wincent/masochist/issues/140, which is about upgrading Relay.

That ticket is about moving from v1.7.0 (which I have been on for years now) to v13.0.0 (which will be out soon). Now, there have been lots of great improvements made in that interval, but also breaking changes which would be quite painful for me to have to accommodate. When you look at what I actually need for my incredibly static, unsophisticated website, it's clear that I am only using about 5% of what Relay has to offer, and that I can probably obtain a better result by building the thinnest of GraphQL data-fetching layers and saving myself from downloading a bunch of JS that I will never need.

Specifically, if you look at the queries we run and the types we have defined in our schema, we have:

So, overall, our data is pretty shallow, with no real nesting of refetchable nodes within nodes. That is, we don't even really need a normalized store — a store of unflattened node data would actually be almost enough to handle the common cases (technically, there are places where we do a partial fetch of a node — such as on the wiki index page, or the search results, or a tag listing — and then need to fetch more data in order to navigate to the permalink/detail view, but these hardly warrant a fully normalized store). Really the only "tricky" requirement we have is to deal with connections, and as already stated, the connections we're using in this app are the simplest possible kind. There are no mutations, no GraphQL subscriptions, no local schema, no need for integration with React Suspense, no need for any kind of configurability, and we don't need garbage collection. We can live without inline GraphQL fragments, using colocated .graphql files instead, meaning we don't need a Babel plug-in.

Given all this, I think we could do everything we need with a few kilobytes of JS, and with something so minimal, we'd be in a good place to do the silly experiments that I want to try out with streaming responses (although the payoff is expected to be tiny, given the lack of size/complexity in the payloads we're dealing with, so I might not even do that).

What we need to have parity with existing solution

What extras we can get with a new solution

Closes: #140

[^jit]: Just saw on HN a related idea in the form of zalando-incubator/graphql-jit, which generates query functions that can be run instead of using the standard generic GraphQL execution implementation in graphql-js.

wincent commented 2 years ago

I should add that lately I've even been tempted to replace all of this with a static site generator, as the only part of the site that actually has to be dynamic is the search functionality, and I think I can find enough interesting subproblems on the static generation side to make it scratch the itch that motivated me to build this stuff in the first place — that is, I can still use my fancy GraphQL tooling to drive the generation, and the schema in its current form (ie. the connections etc) is still probably useful as a data source. As much fun as it would be to play with streaming responses and so on, it would be hard to beat the performance of static HTML with probably less than 1KB of JS to handle the dynamic bits...

I have never been super fond of the "load more" pattern used in the index pages. The blog index is probably just as best served by serving the current article and then having next/previous links to jump between articles. Likewise for the other indices, like the wiki index, I might just be able to generate a big long static list (which in turn would enable some nice type-to-filter UI). At the time of writing, I have about:

Of that list, only the snippets list is probably unpleasantly large to fetch and render. Anyway, something to think about...

The blog obviously needs a reverse chronologically ordered listing (by creation date), but the truth is the wiki's list (by updated at) is rarely interesting beyond the first page or so. An alphabetical list might be useful as an alternative view.

wincent commented 1 year ago

Just wanted to note that I'm still toying with the idea of generating static HTML, although also weighing that up against other things like using React Server Components (see RFC).

I care about three things:

  1. Initial page load time.
  2. Navigation time.
  3. Deploy time.

I'll weigh any potential architecture against those three before proceeding. Some example thoughts:

wincent commented 4 months ago

Going to spitball some ideas here about what to do with my schema artifact. I've been thinking about this for a few days now (not actively, but wondering if my subconscious mind will deliver an answer to me after I wake up). Let's compare some approaches and examples of libraries and tools that embody those approaches:

Now, I don't want to go the graphql-js path — neither by consuming it as a library (I would not have written the lexer/parser unless I'd wanted to get rid of the dependency on graphql-js) nor by copying the pattern (whatever code I end up having to write to provide resolvers, I want it to be close to a POJO as possible, because one of the motivations for doing this rewrite in the first place is to provide me with a vehicle for trying out novel execution strategies[^alluded] — class-based hierarchies tend to make that kind of iteration and experimentation harder rather than easier). But most of the other solutions appear to barely leverage the schema, and leave opportunities for code generation on the table (indeed, if some of them offer any type safety features, they don't make it clear, which is a shame given one of the main selling points of GraphQL is that it's a type system which lends itself to static verification and generation). graphql-code-generator seems to be the exception.

It goes without saying that I want the resolvers to be 100% typesafe. But maybe less obviously (although mentioned elsewhere), I think that I might be able to make the resolver layer a particularly thin one by instead focusing on building underlying abstractions for loading objects and collections of objects. By a combination of naming conventions and directives, I might be able to wire up the resolvers to the low-level data fetching code without any code at all (or 1 layer of indirection to make things static enough for the TypeScript compiler to verify).

Now, you could also make that lower-level layer and use it usefully with any of the other solutions on the list, so there's not really any significant risk with making a start on building that layer as a first step. I think the end destination ends up being something that looks a bit like graphql-code-generator (and of course, I'm not going to use that but will instead roll my own, as GOD intended).

[^alluded]: Alluded to in a number of places, but including things like streaming responses, generated resolvers, custom endpoints for specific persisted queries etc.