Page MenuHomePhabricator

Decide which server rendering methods to use in which cases
Open, Needs TriagePublic

Description

https://www.mediawiki.org/wiki/Design_System_Team/Server_rendering_background_info identifies several possible methods for rendering reusable UI components on the server:

  • Client-side only: don't render anything on the server. This is the simplest to implement, but makes the UI unusable for users of older browsers or users without JavaScript.
  • Server-side only: don't use any JS-based interactivity at all. This requires HTML+CSS-only implementations of (some) UI components, and limits how interactive the UI can be, but it'll work for everyone.
  • Progressive enhancement: render the UI on the server, as above, but add minor interactivity in JavaScript. Works best for simpler UIs with small amounts of interactivity.
  • Separate server and client implementations: render one UI on the server, then another more feature-rich one on the client. Requires a lot of duplicate work.
  • Vue server-side rendering (SSR): implement the UI once in Vue.js, use Vue's SSR feature in Node.js to render a static version of it on the server, and then run the same code on the client for the interactive version.

(For more information about each of these approaches, see https://www.mediawiki.org/wiki/Design_Systems_Team/Server_rendering_background_info )

These aren't meant to be mutually exclusive options. The idea is not to choose one option and reject the others. Instead, I'd like to try to identify which approach should be used in which situation (and maybe some of these approaches should be avoided / discouraged / not be used).

Please give your input on these questions:

  • Are these the right approaches to look at? Are there other approaches that we should consider?
  • Is more information needed about any of these approaches? Do you have pros/cons/considerations to add?
  • In your view, when should which approach be used? Are there any approaches that you think should not be used?
  • Is anything unclear to you? Do you need more information? Do you have any questions?

I hope that this will spark a discussion which will lead to a (rough) policy for when it's appropriate to use which rendering method. Once we have that, we can then delve into the architecture and implementation details of each approach where needed.

Event Timeline

Here's a straw-person proposal to spark discussion (please criticize liberally):

I think we should mostly use these three approaches:

  • Client-side only: Should generally be avoided, but can be used if a feature is so reliant on JavaScript or modern browser support that a meaningful non-JS alternative isn't feasible. A good example of this is VisualEditor, which is client-side only for that reason. The notifications popup also kind of falls in this category (without JS, the bell icon is just a link to Special:Notifications)
  • Server-side only: The best option in cases where no interactivity is needed. For example, rendering simple icons, buttons and links in the skin.
  • Vue SSR: The best option for more complex UIs, where the non-JS version is similar to or a subset of the JS-based UI. Good examples are MediaSearch, the GrowthExperiments homepage, Special:Notifications, Special:RecentChanges, and most other complex special pages. The search bar (with suggestions as you type) would also be a good use case for this. However, whether we can use Vue SSR in all of these cases also depends on how easy we can make it to install SSR for third-party MediaWiki setups: if we can't make installing a Node.js-based SSR feature a requirement for installing MediaWiki, then we either can't use Vue SSR for core functionality like Special:RecentChanges, or we have to implement a non-SSR fallback version.

I think we should rarely, if ever, use the other approaches:

  • Separate server and client implementations: we should avoid this where possible, and prefer Vue SSR. We may need to use separate implementations if Vue SSR can't be used, or if it's important to have a fallback implementation case Vue SSR is unavailable (not installed or experiencing downtime)
  • Progressive enhancement: I would prefer to avoid the "render HTML in PHP and enhance it with JS code that grabs things from that HTML" model of progressive enhancement. Instead, I think we should use Vue SSR, or client-side only features with a simple non-JS fallback (e.g. what we currently have for the notifications dropdown)

For all of these, but for Vue SSR especially, I think we should use the "islands of interactivity" model explained in this blog post. I think it's too early to consider making the entire MediaWiki page a Vue component. Instead, I think we should continue to render MediaWiki pages in PHP the way we do now, and have that PHP code call Vue to render portions of the page.

  • Client-side only: Should generally be avoided, but can be used if a feature is so reliant on JavaScript or modern browser support that a meaningful non-JS alternative isn't feasible. A good example of this is VisualEditor, which is client-side only for that reason. The notifications popup also kind of falls in this category (without JS, the bell icon is just a link to Special:Notifications)

There is another set of features that can be only client-side: If the rendered content is user specific(for example - a dashboard for the current user), there is little caching advantage. Hence rendering it at client side may be acceptable. Generalizing this, can we elaborate on the caching aspects of SSR? It is currently missing in the background info page as well.

Do we have any rough idea about data communication to the rendering service? How do we pass the data required for rendering the UI from a Mediawiki context to that service?

In a real world use case, if the final page we render to user is a result of more than one Vue application(could be multiple extensions), how do we consolidate the HTML+CSS and put it back to the Mediawiki output page?

Let me write down a dummy workflow, just to get clarity. Please correct me if I get this completely wrong.

  1. Extension1, Extension2.. ExtensionN registers their SSR capability to Mediawiki,
  2. For all these registered extensions, SSR is invoked, by passing the required data- could be a render hook?
  3. Retrieve the HTML and put it back to output page.
  4. Each extension is responsible for the hydration for their island.

The "islands of interactivity" model for us would be multiple UI parts from same extension or multiple extensions. This article outlines a global data store and single mechanism to hydrate all islands. I have difficulty in imagining how that can be achieved in a multi-extension scenario like ours. Our current Vue model for front end involves multiple independent Vue applications that share nothing right?

I know, this is mostly "How" questions, but I am trying to get a clear picture of how all these work in a real world setup to evaluate options.

One of the inputs for FAWG was the desire of product teams to be able to write UI entirely in JavaScript. The need to implement a component twice, once in PHP and once in JS, was seen as a major probelm. And even if some components would only be needed in PHP, and others only in JS, the switching was seen as at least annoying.

If the ability to write UI entirely in JavaScript is still desirable (or even a requirement), then this only leaves two options: Client-Side only, or Vue SSR. Since Client-Side only would not acceptable at least for the navigational chrome (aka skin) for regular page views, that only leaves Vue SSR.

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well: it enforces an API-first, strictly layered approach for MediaWiki's backend. I think of it as "frontend decoupling". It would enable us to build multiple frontend applications tailored to individual use cases, instead of the one-size-fits-all approach we currently have.

Of course, we would still need to support HTML rendered by MW itself, be it the page content or special pages or other chunks of content. So one important component would be a composition layer that combines chunks of HTML coming from different places. This layer could be inside MediaWiki (so MW would call servies that render components) or on top of MediaWiki (maybe a node.js based application calling into MW and otehr services), or at the network edge (using ESI).

I am in favor of the proposed approach in the SOW, i.e. Server-side only provided the benefits outweigh potential overhead costs. Whether the migration of the UI task on the server side is actually needed or not needs a trade off or cost function analysis- old browser support Vs costs associated to data communication and more server-side load, which is missing in the SOW's background.

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

While I understand this, and I think it definitely makes sense for some tools (specially advanced tools for power users), I need to object to this change for "all or most features".

Unlike services like Uber, Amazon, etc. that target industrialized countries with good computer hardware available to everyone. Wikipedia's mission is to bring knowledge to not-so-developed countries too. We have considerable traffic from those countries with old versions of IE and according in-person surveys and research, we know for a fact that they read Wikipedia differently than someone living in Germany, responders mentioned that they read Wikipedia to learn high school or university materials (or learn materials they need for personal growth in general) since there is no better option is available (like a library). This is of vital importance when thinking about building js-only features that we are helping reducing global inequality in access to knowledge and we should not turn away from that.

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

While I understand this, and I think it definitely makes sense for some tools (specially advanced tools for power users), I need to object to this change for "all or most features".

Unlike services like Uber, Amazon, etc. that target industrialized countries with good computer hardware available to everyone. Wikipedia's mission is to bring knowledge to not-so-developed countries too. We have considerable traffic from those countries with old versions of IE and according in-person surveys and research, we know for a fact that they read Wikipedia differently than someone living in Germany, responders mentioned that they read Wikipedia to learn high school or university materials (or learn materials they need for personal growth in general) since there is no better option is available (like a library). This is of vital importance when thinking about building js-only features that we are helping reducing global inequality in access to knowledge and we should not turn away from that.

+1. About old versions of IE (for Vue support): T301128: research: Understand how current and new users of Growth features would be affected by switching to Vue and T301128: research: Understand how current and new users of Growth features would be affected by switching to Vue (T301128#7995103) are worth looking at, with the caveat that we were analyzing logged-in user traffic only.

Wikipedia's mission is to bring knowledge to not-so-developed countries too.

This is a great point, and I believe it's compatible with the goal of this task if diligently include this "DOM-only" use case as a requirement for SSR. Interfaces can be built in Javascript, rendered on the server, and sent to clients as HTML for which the client JS only adds progressive enhancement.

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

While I understand this, and I think it definitely makes sense for some tools (specially advanced tools for power users), I need to object to this change for "all or most features".

Perhaps my statement was misleading - this was intended as an argument supporting the need for server-side rendering, so no JS support would be needed on the client, even if we implement the UIs entirely in JS. I hope that Vue.js is flexible enough to render into HTML that support progressive enhancement - e.g. my thining was that the vue.js component for the search box would be rendered to HTML on the server side, and on the client side would become an interactive vue component if the browser supports it, and stay a plain old html form field for browsers that don't.

Similarly, if we construct the sidebar from vue.js components, I would expect them to render into static HTML usable with JS disabled.

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

My interpretation of this statement was we'd use Node.js on the server side to render it - so one language for both backend and frontend. Did I misunderstand?

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

While I understand this, and I think it definitely makes sense for some tools (specially advanced tools for power users), I need to object to this change for "all or most features".

Perhaps my statement was misleading - this was intended as an argument supporting the need for server-side rendering, so no JS support would be needed on the client, even if we implement the UIs entirely in JS. I hope that Vue.js is flexible enough to render into HTML that support progressive enhancement - e.g. my thining was that the vue.js component for the search box would be rendered to HTML on the server side, and on the client side would become an interactive vue component if the browser supports it, and stay a plain old html form field for browsers that don't.

Similarly, if we construct the sidebar from vue.js components, I would expect them to render into static HTML usable with JS disabled.

Ideally, I think this should work like React's new server components feature[1], i.e. that there should be a possibility to write "server-only" components in the JS stack that do not result in additional JS delivered to the client irrespective of client capabilities. Full rehydration requires the client to effectively redownload the page HTML one more time as JS & reconcile it with the SSR'd output, which is not great.


[1] https://github.com/reactjs/rfcs/blob/2348bd8ed7fb66fedf04726eb046065be7f4e23f/text/0188-server-components.md

Ideally, I think this should work like React's new server components feature[1], i.e. that there should be a possibility to write "server-only" components in the JS stack that do not result in additional JS delivered to the client irrespective of client capabilities. Full rehydration requires the client to effectively redownload the page HTML one more time as JS & reconcile it with the SSR'd output, which is not great.

I'm very interested in providing this kind of capability to developers, and I think it fits in well with the "islands of interactivity" architecture that @Catrope mentioned above.

In my mind, the ideal developer experience for this feature would be having access to some kind of renderVueComponent() method from OutputPage. In addition to taking a component tree, this method could accept some options about what kind of hydration behavior should be applied:

  • The component should hydrate immediately, as soon as JS is initialized
  • The component should hydrate lazily, when requestIdleCallback is fired
  • The component should hydrate when it is about to enter the viewport, using an IntersectionObserver
  • The component should never hydrate, and should exist as static markup on the page

I think that the Astro framework provides a nice example of such a system in practice (see their client directive docs). We could theoretically implement something similar in PHP.

Such an approach seems like a good fit to the MediaWiki use-case, where we have different teams managing different extensions, any number of which might be active on a given Wiki page. Rather than tying things together into a single app, this would allow each project to specify its own hydration behavior in a way that hopefully reduces conflicts and performance issues.

  • Client-side only: Should generally be avoided, but can be used if a feature is so reliant on JavaScript or modern browser support that a meaningful non-JS alternative isn't feasible. A good example of this is VisualEditor, which is client-side only for that reason. The notifications popup also kind of falls in this category (without JS, the bell icon is just a link to Special:Notifications)

There is another set of features that can be only client-side: If the rendered content is user specific(for example - a dashboard for the current user), there is little caching advantage. Hence rendering it at client side may be acceptable.

The fact that some rendered content can't be cached is a good point, but we also have to consider whether the feature needs to be available to non-JS users. For per-user dashboards we may decide that it's not that important, but we probably want Special:RecentChanges to be usable without JS for example (even though it's also pretty much uncacheable).

Generalizing this, can we elaborate on the caching aspects of SSR? It is currently missing in the background info page as well.

This is something I haven't explored much yet. A simple implementation that just replaces existing PHP-based UI rendering code with SSR would have the same caching properties as we have currently. We may want/need to do better than that, and I hope we can develop some ideas around that as part of this process.

Do we have any rough idea about data communication to the rendering service? How do we pass the data required for rendering the UI from a Mediawiki context to that service?

I have a prototype that illustrates how this could work. See T286963 for details and links. In my prototype, MediaWiki sends HTTP POST requests to a Node.js service, with the request body containing the code of the Vue component to be rendered (+its dependencies), and the parameters (props) to pass to the component. The service then executes this code and returns the rendered HTML. I don't think this is an ideal solution, because sending the full code every time is wasteful and also not great from a security perspective. Other alternatives I'm going to explore (and may prototype) are:

  • The service fetches the code by making HTTP requests to load.php, essentially mimicking what a browser would do
  • MW shells out to a command-line tool written in Node.js that does approximately the same thing as the service does
  • MW uses php-v8js to run the JS code directly in-process (T286966)

To make SSR easier to install for third-party wikis, we could also implement two approaches: one for use in production (that performs/scales better but may be harder to set up) and one for use on smaller third-party sites (that may not be as fast/scalable but is easier to set up / has fewer dependencies).

In a real world use case, if the final page we render to user is a result of more than one Vue application(could be multiple extensions), how do we consolidate the HTML+CSS and put it back to the Mediawiki output page?

My prototype proposes the same thing that @egardner talked about in his comment (directly above this one): an OutputPage::renderVueComponent() method that the MW PHP code can use to delegate rendering of a part of the page to SSR. The MediaWiki PHP application would still render the overall page, but for the portion of the page that uses SSR it would call renderVueComponent(), which would send a request to the SSR service, get HTML back, and output that HTML along with a JS snippet that manages hydration. That approach would be the easiest to implement and adopt and cause the least disruption, but we should consider other approaches as well.

For example, the workflow you laid out in your comment suggests that multiple SSR instances on the same page could be bundled and sent to the service together, perhaps by injecting placeholders at first and replacing those with the rendering result later.

Let me write down a dummy workflow, just to get clarity. Please correct me if I get this completely wrong.

  1. Extension1, Extension2.. ExtensionN registers their SSR capability to Mediawiki,
  2. For all these registered extensions, SSR is invoked, by passing the required data- could be a render hook?
  3. Retrieve the HTML and put it back to output page.
  4. Each extension is responsible for the hydration for their island.

And as @daniel points out in his comment, resolving SSR placeholders could happen in MW itself, but could also happen in a composition service that wraps MW, or in the edge caching layer:

Of course, we would still need to support HTML rendered by MW itself, be it the page content or special pages or other chunks of content. So one important component would be a composition layer that combines chunks of HTML coming from different places. This layer could be inside MediaWiki (so MW would call servies that render components) or on top of MediaWiki (maybe a node.js based application calling into MW and otehr services), or at the network edge (using ESI).

I'm personally inclined to keep this in MW for now, as I think the complexity of wrapping MW in a composition service wouldn't be justified unless/until we also have other uses for it (e.g. better caching of page views for logged-in users, with the user-specific details filled in by a composition service or at the edge), and once that happens SSR could be moved from MW into that composition layer.

  1. Each extension is responsible for the hydration for their island.

In general, I'd like hydration to happen automatically. Vue's default hydration behavior is easy to use, and requires boilerplate code that can be generated automatically: all you need to do is make sure that the Vue component's JS code that it was rendered from on the server is also loaded on the client, and then call Vue.createSSRApp( component, props ).mount( '#wrapper' ), where component is the Vue component implementation, props is the data that was passed into the component, and wrapper is the ID of the HTML element that contains the server-rendered HTML. In my prototype, I wrote a function that does these things, and render a small script tag that calls it as part of the SSR output.

But I agree with @egardner that we should give each extension/caller control over when its output is hydrated:

In my mind, the ideal developer experience for this feature would be having access to some kind of renderVueComponent() method from OutputPage. In addition to taking a component tree, this method could accept some options about what kind of hydration behavior should be applied:

  • The component should hydrate immediately, as soon as JS is initialized
  • The component should hydrate lazily, when requestIdleCallback is fired
  • The component should hydrate when it is about to enter the viewport, using an IntersectionObserver
  • The component should never hydrate, and should exist as static markup on the page

We could consider adding to this list something like "The component should hydrate when it is interacted with, i.e. when a specified event (e.g. click) is fired on a specified selector (e.g. #searchInput)".

The "islands of interactivity" model for us would be multiple UI parts from same extension or multiple extensions. This article outlines a global data store and
single mechanism to hydrate all islands. I have difficulty in imagining how that can be achieved in a multi-extension scenario like ours. Our current Vue model for front end involves multiple independent Vue applications that share nothing right?

Yes, currently our model is to have multiple independent Vue applications on the same page. I think they should be hydrated separately, but that hydration process can be managed globally, as I described above. A global data store may be useful in some cases, and we could explore that later, but I don't think that it's all that important right now. It would become more important if/when parts of the skin use Vue (right now only the search bar does, and it probably doesn't need to listen to anything from a global store). If multiple "islands" on the same page need to share a store to communicate with each other, we could require them to set that up manually for now, and implement a global store once we have a clearer idea of what it would be used for.

I know, this is mostly "How" questions, but I am trying to get a clear picture of how all these work in a real world setup to evaluate options.

No worries! I had suspected that separating the high-level and low-level discussions could be hard given that the properties of the SSR implementation could influence when we would want to use it. At least I tried :)

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

While I understand this, and I think it definitely makes sense for some tools (specially advanced tools for power users), I need to object to this change for "all or most features".

Perhaps my statement was misleading - this was intended as an argument supporting the need for server-side rendering, so no JS support would be needed on the client, even if we implement the UIs entirely in JS. I hope that Vue.js is flexible enough to render into HTML that support progressive enhancement - e.g. my thining was that the vue.js component for the search box would be rendered to HTML on the server side, and on the client side would become an interactive vue component if the browser supports it, and stay a plain old html form field for browsers that don't.

Yes, that's right: Vue SSR returns an HTML string that is the same as what a browser would produce when rendering the initial state of the component. In a browser that supports modern JS, it's then hydrated and becomes an interactive component, without a flash of unstyled content or loss of interaction status (e.g. if the user has a text input focused or is typing into it, that won't be disrupted by the hydration process). In a browser that doesn't, that last step never happens and the component doesn't become interactive. If the component is well-written, that non-interactive HTML+CSS-only output still makes for a usable UI.

It's also important to remember that, even in browsers that do support modern JS, hydration is not immediate (because loading JS takes time, and because we may choose to hydrate some components lazily), so all users will see and use the unhydrated ("dry"? "dessicated"?) version of the component at least some of the time, unless we specifically prevent that on a feature-by-feature basis.

Similarly, if we construct the sidebar from vue.js components, I would expect them to render into static HTML usable with JS disabled.

I support @TK-999's and @egardner's responses to this: we should offer SSR with no hydration as an option.

A quick meta-point:

I know, this is mostly "How" questions, but I am trying to get a clear picture of how all these work in a real world setup to evaluate options.

No worries! I had suspected that separating the high-level and low-level discussions could be hard given that the properties of the SSR implementation could influence when we would want to use it. At least I tried :)

One bit of wisdom I took from The Art of System Architecture is that in order to come up with a good design, one needs to constantly zoom in on details, then zoom back out on the big picture, then zoom in on details again - because the implementation details impact the architecture, and the architecture impacts the implementation. That is to say, we should be mindful of the different levels of discussion, and avoid mixing them too much, but we shouldn't keep them separated too much, or neglect either level.

To me, being able to build UI elements and skins entirely in JS is very appealing from an architectural perspective as well

While I understand this, and I think it definitely makes sense for some tools (specially advanced tools for power users), I need to object to this change for "all or most features".

Unlike services like Uber, Amazon, etc. that target industrialized countries with good computer hardware available to everyone. Wikipedia's mission is to bring knowledge to not-so-developed countries too. We have considerable traffic from those countries with old versions of IE and according in-person surveys and research, we know for a fact that they read Wikipedia differently than someone living in Germany, responders mentioned that they read Wikipedia to learn high school or university materials (or learn materials they need for personal growth in general) since there is no better option is available (like a library). This is of vital importance when thinking about building js-only features that we are helping reducing global inequality in access to knowledge and we should not turn away from that.

This point is almost completely moot. While I 100% support the idea we don't build wikipedia only for the western world, old versions of IE would run on old OS versions that don't support TLS 1.2 natively, which means that those users could not connect to our sites, see https://wikitech.wikimedia.org/wiki/HTTPS/Browser_Recommendations.

If anything, supporting those users, who most likely don't have powerful/new computers, would suggest we should work hard to remove the amount of client-side rendering that is performed.

A few important questions could help us understand what direction we're going to go to:

  • If the idea is to create a pure lambda service (mediawiki calls the SSR service, gets a response within a timeout, else returns a fallback page that will use client-side rendering), we should work hard to avoid having multiple calls per page render to the SSR component. If we allow all extensions/components of mediawiki to make independent calls, we might end up with pathological situations where we make 100s of calls to the service to render a page and that takes 10 or more seconds as a result. The SSR needs to happen at a specific stage of page composition and should be a single call (or a fixed number of calls anyways). I'm also unsure we want to ship all the JS code to execute from mediawiki to the service
  • If we want to make this SSR component actually a frontend compositor, that basically calls mediawiki and based on its response builds the page, we would probably need to instead be more careful with caching logic and invalidation logic; The service would also have stricter reliability requirements

Additionally:

  • We need to define what performance penalty is acceptable for doing SSR. Say our average page view takes 400 ms right now; are we ok with a 10% performance penalty? That should inform the SLOs for the new service
  • We really need not only to declare the service loosely coupled to mediawiki, but ensure that property stays long-term; which means that if the SSR fails, the html page created by mediawiki should be able to be rendered client-side to the same standards of the server-side rendering
  • We need to have a quick handle in mediawiki that switches SSR on/off in production in case of problems with the service

Going a bit more high-level, this approach will create a lot of busywork server side, and make our sites slower and less reliable[1]. It will take a lot of effort and care to make those effect negligible. I'm not sure we answered the question "is it worth it"? I understand and value development experience and velocity, but it shouldn't be the only thing guiding us.

[1] Please note: the reliability loss is unavoidable when we add an additional independent component to rendering the pages; the loss of performance too, unless we're substituting the php frontend with a new frontend written separately and that only interacts with mediawiki with API calls. In that case it's theoretically possible to make a more performant frontend, it's just prohibitively difficult.

One more thing to consider - HTML is currently cached on the edge for up to 24h, while changes to JS assets are propagated to users by ResourceLoader in 5-10mins after a deployment. This can result in a situation where a new version of JS (Vue components) is running against and trying to hydrate an old HTML structure. Does Vue handle this gracefully, or would it result in a full client-side rerender of the affected component(s)? If yes, is that acceptable?

Going a bit more high-level, this approach will create a lot of busywork server side, and make our sites slower and less reliable[1]. It will take a lot of effort and care to make those effect negligible. I'm not sure we answered the question "is it worth it"? I understand and value development experience and velocity, but it shouldn't be the only thing guiding us.

Yes, this is a question we need to place front-and-center throughout this process. There are alternatives to building out a SSR service and writing more and more of our UI in Vue components. I could easily imagine an alternative scenario where we do the following instead of pursuing SSR:

  • Development of Codex Vue.js components would continue, but their use would be limited to highly-interactive scenarios (dashboards, certain Special pages, etc).
  • To ensure visual consistency between client-rendered and server-rendered parts of our UI, we'd need to build out a robust set of CSS-only "components" that replicate the appropriate styles – sort of like a Codex "Bootstrap" approach. I should mention that the Design Systems Team is already looking into such a project (see T321351: [EPIC] Add CSS-only components), although we are not yet assuming there will be a 1:1 correspondence between CSS and Vue implementations
  • In cases where we need to "sprinkle in" interactivity into a mostly server-rendered page (perhaps we are talking about an article page or some other performance-critical view of our application), we could retain some of the benefits that Vue gives us in terms of declarative UI development and reactive handling of user updates by looking at something like petite-vue, an alternative subset of Vue.js that is optimized for progressive enhancement. I wrote more about such an approach here: T291666: Create a corresponding set of "lite" components suitable for progressive enhancement with petite-vue. We could probably improve the developer experience a great deal by upgrading our very basic Mustache template system with something a little more modern like Twig that would allow us to share component-like partials and pass parameters to them.

Just like SSR, this alternate approach has its own pros and cons; in this case we'd be getting architectural simplicity and avoiding performance issues at the cost of a more fragmented and arguably less ideal developer experience – but these problems could likely be mitigated (and I hope we would invest resources in attempting to do that) if we decided this was the best way forward.

A few important questions could help us understand what direction we're going to go to:

  • If the idea is to create a pure lambda service (mediawiki calls the SSR service, gets a response within a timeout, else returns a fallback page that will use client-side rendering), we should work hard to avoid having multiple calls per page render to the SSR component. If we allow all extensions/components of mediawiki to make independent calls, we might end up with pathological situations where we make 100s of calls to the service to render a page and that takes 10 or more seconds as a result. The SSR needs to happen at a specific stage of page composition and should be a single call (or a fixed number of calls anyways).

I had been thinking that the simpler approach of making calls to the server inline without needing to do placeholders and request bundling was going to be OK, but you've convinced me that we should avoid that. We may be able to get away with not doing this if the request overhead (the difference in timing between making two separate requests vs making one combined request) is negligible, but it probably won't be, and implementing bundling won't be very difficult. For the initial use cases I think it'd be rare to have multiple requests on the same page, but in the future it may not be (especially if the skin starts using SSR), and we should future-proof this.

I'm also unsure we want to ship all the JS code to execute from mediawiki to the service

Yeah we definitely shouldn't do that IMO. I did that in the prototype because it was easy, but it raises security concerns and is also slow. All that code needs to be transferred over the network (yes it's a local network or loopback interface but still), parsed, and executed anew, every time you make a request. From my local testing that appears to be really bad for performance, unsurprisingly.

  • If we want to make this SSR component actually a frontend compositor, that basically calls mediawiki and based on its response builds the page, we would probably need to instead be more careful with caching logic and invalidation logic; The service would also have stricter reliability requirements

Agreed. I'm not proposing that for now, mostly for those reasons and because it would be more disruptive for not much benefit (except for related but out-of-scope things like caching pageviews for logged-in users).

  • We need to define what performance penalty is acceptable for doing SSR. Say our average page view takes 400 ms right now; are we ok with a 10% performance penalty? That should inform the SLOs for the new service

I'm going to be doing some prototyping experiments this week to both address and measure SSR performance, and try to solve the "don't ship all the code to the service" problem. That should give us an idea of what kind of performance penalty we'd be looking at. I'm using https://en.wikipedia.org/wiki/Special:MediaSearch as my test case.

  • We really need not only to declare the service loosely coupled to mediawiki, but ensure that property stays long-term; which means that if the SSR fails, the html page created by mediawiki should be able to be rendered client-side to the same standards of the server-side rendering

Definitely. It's not just that SSR can be installed or not installed, the service can also fail at any time (due to bugs in the code being SSRed, or bugs in the SSR service, or unreliability). Thankfully it's relatively easy to address this: Vue's built-in hydration will always reproduce the same UI as the server-rendered one, even if there's no SSR output (and in fact I think the default fallback should just be an empty box, which then gets populated with the intended UI client-side). But we may have to adjust when we do hydration if there's an error (e.g. not deferring it even if deferral was requested).

  • We need to have a quick handle in mediawiki that switches SSR on/off in production in case of problems with the service

Great point! I didn't think of this, but it'll be easy to provide.

Going a bit more high-level, this approach will create a lot of busywork server side, and make our sites slower and less reliable[1]. It will take a lot of effort and care to make those effect negligible. I'm not sure we answered the question "is it worth it"? I understand and value development experience and velocity, but it shouldn't be the only thing guiding us.

Yes, this is a question we need to place front-and-center throughout this process. There are alternatives to building out a SSR service and writing more and more of our UI in Vue components. I could easily imagine an alternative scenario where we do the following instead of pursuing SSR:
[...]

As I said at the top of this task, one of my premises for this discussion was that we'd probably select different approaches for different cases. We could decide that SSR is the best option in some cases, but not in other cases. Some of the things @Joe points out are fixed costs that we'd incur from using SSR at all, regardless of how much it's used, but some of them aren't and could be mitigated by avoiding SSR in cases that are performance-sensitive or where it's not worth it for some other reason. (If you disagree with that premise, please speak up about that too!)

One more thing to consider - HTML is currently cached on the edge for up to 24h, while changes to JS assets are propagated to users by ResourceLoader in 5-10mins after a deployment. This can result in a situation where a new version of JS (Vue components) is running against and trying to hydrate an old HTML structure. Does Vue handle this gracefully, or would it result in a full client-side rerender of the affected component(s)? If yes, is that acceptable?

This is an excellent point: using SSR with hydration runs into exactly that complication in cases where the output is cached. This would be a problem in some situations (e.g. anonymous page views, maybe logged-in ones in the future) but not in others (e.g. most special pages). Currently, we already have to be careful when making HTML+CSS changes in situations where the HTML is cached (we have to write backwards-compatible CSS).

Vue handles this situation gracefully, in that it will attempt to hydrate the server-rendered HTML, notice the mismatch, and correct it by discarding and rerendering the mismatched portions. This does degrade client-side rendering performance (because rerendering the mismatched parts is more expensive than reusing the server-rendered HTML; Vue tries to only rerender the parts that need it, but it doesn't always detect that correctly and sometimes rerenders more than it needs to), and may impact the user experience because e.g. input focus states may not be preserved, or visible jumps may occur (if the old version and new version look different, you'll see the old version briefly before it's replaced with the new one), but the end state will be correct.

You can read more about this in the Vue documentation here (that page focuses more on randomness and time-related issues as causes for hydration mismatches, it doesn't discuss caching as a possible cause, but the discussion of how Vue handles it still applies).

Also note that, if we use SSR for something that then isn't hydrated at all (as discussed earlier on this task), this issue wouldn't arise in those cases. We'd still have the issue of the CSS being newer than the HTML, but we already have that issue today.

As this discussion seems to have stalled out a bit, I thought I'd summarize what I've heard so far.

About Vue SSR specifically:

  • People asked how multiple extensions using SSR on the same page would work. Giuseppe convinced me that request bundling is needed.
  • Some sort of composition layer is needed. This could be inside MW, or something that wraps MW. The latter would be harder due to caching and reliability requirements, so I prefer the former (at least to start with; if an independent need arises for a composition service later, we can move SSR into it).
  • We should support delayed hydration, or not hydrating something at all
  • Consider caching issues: cached HTML may get out of sync with JS
  • We need to account for the SSR service being down sometimes (or not installed at all)
  • We need to set a performance budget (I will work on measuring my prototype's performance, and prototyping different architectures that should perform better)

About which approach to use when:

  • People say this is hard to talk about without more specifics about the proposed SSR architecture
  • Being able to build front-ends in just one language (JS) is desirable
  • An alternative to using SSR is to use the CSS-only Codex components that the Design Systems Team is considering developing, possibly combined with petite-vue and Twig

Next, I will work on a prototype and a more concrete proposal for what Vue SSR would look like, along with a more concrete proposal for when we'd use it vs when we'd use something else.

In the meantime, please continue to weigh in if you want! Comments like "I am working on / plan to work on X feature, and I would like to use Vue SSR / use something else" are also very valuable, and we haven't gotten any of those yet.

Comments like "I am working on / plan to work on X feature, and I would like to use Vue SSR / use something else" are also very valuable, and we haven't gotten any of those yet.

We would use SSR to render GrowthExperiments' Suggested Edits module on Special:Homepage, I am not sure we would bother try to do any Vue port without SSR being available.

In the meantime, please continue to weigh in if you want!

Something that's been back of my mind in this discussion: what are the service boundaries for code that runs in the server-side rendering context? Once we have a (presumably) node app running somewhere to provide SSR, are we going to see pushes to add nodejs modules that would allow for DB/file system access? It might be good to get consensus up front about what is/isn't allowed.

ldelench_wmf moved this task from Inbox to Needs Refinement on the Design-System-Team board.
ldelench_wmf added a subscriber: LNguyen.