Problem
On the web app we have two major issues: performance and code complexity. This project proposes steps to address both.
The problems in this space are clear, but the solution requires more research to be fully shaped.
Web Load Performance
When a document is loaded by the current web code, we have a very bad experience where the query blocks are not loaded by the initial server render. Same with embeds and mentions- the content is loaded , which gives a slow and jumpy reading experience.
This compromise was made in development because the embeds are located deep down in the component hierarchy and it would be highly inconvenient to try and hoist the data loading up to the top. We tried this with supportDocuments and supportQueries but the code felt very unmanageable, because desktop embed components were responsible for their own data loading. This led to fragile loading code and fragmentation of embed components between platforms with ugly dependency injection of those fragmented components.
In a more general sense, this is known as "the waterfall loading pattern" where the client must wait on several generations of requests before the data is ready for the user. Ideally the client could make a single round-trip to the server to load all the data at once.
Confusing Loading Code
Documents are loaded on the server side with the remix loader, while comments are loaded on the client with React Query.
But sometimes we have data that is used by the server rendering that is also mutable in the front end. In that case we need to use both data loading techniques, causing confusion for the devs, and forces us to write redundant/fragile code.
The profile page on web is such an example. Roughly:
export const loader = async () => { // remix server loader
// loader logic, returns profile
return dataResponse(await loadProfile())
}
export default function ProfilePage() {
const serverProfile = useLoaderData() // remix data
const clientProfile = useQuery({...) // React Query
const displayProfile = clientProfile.data || serverProfile
return <ProfileView profile={displayProfile} />
}This code allows server rendering and also the client mutation with the normal React Query workflow. But it is overly complicated because there are two entirely different code paths to load the same piece of data!
This problem is deeply connected to our web loading performance issue, because sometimes this profile appears deep down in our component hierarchy and useLoaderData must be used in the web-only container code. If we are not careful, we will make one problem worse by fixing the other one.
Data Normalization
We have troubles when the user mutates content on the web, for example updating a profile. Many of our queries return data about the same underlying resources, such as search, batch getAccount, (and the feed?).
Because our clients use the ReactQuery cache, every query/request is cached separately. So the metadata for a profile (for example) is duplicated in many places around our cache and we need to be careful to keep them in sync.
Currently, when something like the profile changes, we aggressively invalidate all things which might have changed. This aggressive invalidation results in heavier-than-necessary use of our API. We have no strategy for invalidating data loaded by the remix loader.
Other Related Problems
There are other concerns which are low priority, but we should consider while we are re-architecting our data loading:
How to handle API drift where the server and client are out of sync (when we do a deploy, the server runs newer code than the clients that are already open)
How to handle updates from the server when things change- the user should not have to refresh to see the latest content on desktop or web
How does the client communicate to the daemon what the user is looking at- this affects p2p sync because we want the user to see up-to-date content on their screen, even if they are not yet subscribed. Expect another document about this soon!
Solution
This solution describes the rough desired API and code loading pattern, without proposing a specific technology. More research is needed to find the most realistic implementation of these APIs. May be possible with React Query, or may be custom code.
The desired code would look something like this:
export const loader = dataLoader(async (load, request) => {
// this server loader is entirely optional! if you skip this, everything will be loaded with AJAX requests like we do in the desktop app
await load(request.params.profileId)
})
export default const webPageContainer(params => {
const { profileId } = params
return <ProfilePage id={profileId} />
})
function ProfilePage({id}) {
// this code is entirely shared with desktop, including data loading
const profile = useProfile(id)
return <ProfileView profile={profile} />
}To address the issues with performance, here is example pseudo-code for loading a document page, with embeds and mentions properly pre-loaded by the server:
export const loader = dataLoader(async (load, request) => {
// this server loader is entirely optional! if you skip this, everything will be loaded with AJAX requests like we do in the desktop app
const doc = await load(request.params.profileId)
await loadEmbeds(load, doc)
})
export default const webPageContainer(params => {
const { docId } = params
return <DocPage id={docId} />
})
function DocPage({id}) { // shared with desktop
const doc = useResource(id)
return <DocumentContent doc={doc} />
}
// deep within document content, it renders this component
function ContentEmbed({id}) { // shared with desktop
const doc = useResource(id)
return <DocumentContent doc={doc} />
}In this example, the webPageContainer wraps the whole page with a client provider that has preloaded all the embed content. This means that the deep useResource in the <ContentEmbed> has access to the data that was preloaded by the server.
Scope
One week of research for Eric Vicenti. Might result in a solution that is ready to merge, a prototype, or a very clear implementation plan.
Currently due to the data normalization restrictions of React Query, my approach would be to prototype directly on top of Remix, focusing on document loading to fix the embed waterfall issue. Possibly the approach will change within this week.
This will not be a never-ending project, it ends at the end of the week according to the ShapeUp methodology. Hopefully there is a clear result and the problems are solved, but this "green light" will absolutely not last more than one HT cycle.
Rabbit Holes
When making a second page request, or when loading comments, how does the client avoid waterfall requests? Remix provides a solution but we need to figure out how to apply it, or what else to do.
No Goes
Realtime features
Handle API drift or schemas