|
| 1 | +# Important Considerations when Using Angular Universal |
| 2 | + |
| 3 | +## Introduction |
| 4 | + |
| 5 | +Although the goal of the Universal project is the ability to seamlessly render an Angular |
| 6 | +application on the server, there are some inconsistencies that you should consider. First, |
| 7 | +there is the obvious discrepancy between the server and browser environments. When rendering |
| 8 | +on the server, your application is in an ephemeral or "snapshot" state. The application is |
| 9 | +fully rendered once, with the resulting HTML returned, and the remaining application state |
| 10 | +destroyed until the next render. Next, the server environment inherently does not have the |
| 11 | +same capabilities as the browser (and has some that likewise the browser does not). For |
| 12 | +instance, the server does not have any concept of cookies. You can polyfill this and other |
| 13 | +functionality, but there is no perfect solution for this. In later sections, we'll walk |
| 14 | +through potential mitigations to reduce the scope of errors when rendering on the server. |
| 15 | + |
| 16 | +Please also note the goal of SSR: improved initial render time for your application. This |
| 17 | +means that anything that has the potential to reduce the speed of your application in this |
| 18 | +initial render should be avoided or sufficiently guarded against. Again, we'll review how |
| 19 | +to accomplish this in later sections. |
| 20 | + |
| 21 | +## "window is not defined" |
| 22 | + |
| 23 | +One of the most common issues when using Angular Universal is the lack of browser global |
| 24 | +variables in the server environment. This is because the Universal project uses |
| 25 | +[domino](https://github.com/fgnass/domino) as the server DOM rendering engine. As a result, |
| 26 | +there is certain functionality that won't be present or supported on the server. This |
| 27 | +includes the `window` and `document` global objects, cookies, certain HTML elements (like canvas), |
| 28 | +and several others. There is no exhaustive list, so please be aware of the fact that if you |
| 29 | +see an error like this, where a previously-accessible global is not defined, it's likely because |
| 30 | +that global is not available through domino. |
| 31 | + |
| 32 | +> Fun fact: Domino stands for "DOM in Node" |
| 33 | +
|
| 34 | +### How to fix? |
| 35 | + |
| 36 | +#### Strategy 1: Injection |
| 37 | + |
| 38 | +Frequently, the needed global is available through the Angular platform via Dependency Injection (DI). |
| 39 | +For instance, the global `document` is available through the `DOCUMENT` token. Additionally, a _very_ |
| 40 | +primitive version of both `window` and `location` exist through the `DOCUMENT` object. For example: |
| 41 | + |
| 42 | +```ts |
| 43 | +// example.service.ts |
| 44 | +import { Injectable, Inject } from '@angular/core'; |
| 45 | +import { DOCUMENT } from '@angular/common'; |
| 46 | + |
| 47 | +@Injectable() |
| 48 | +export class ExampleService { |
| 49 | + constructor(@Inject(DOCUMENT) private _doc: Document) {} |
| 50 | + |
| 51 | + getWindow(): Window | null { |
| 52 | + return this._doc.defaultView; |
| 53 | + } |
| 54 | + |
| 55 | + getLocation(): Location { |
| 56 | + return this._doc.location; |
| 57 | + } |
| 58 | + |
| 59 | + createElement(tag: string): HTMLElement { |
| 60 | + return this._doc.createElement(tag); |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +Please be judicious about using these references, and lower your expectations about their capabilities. `localStorage` |
| 66 | +is one frequently-requested API that won't work how you want it to out of the box. If you need to write your own library |
| 67 | +components, please consider using this method to provide similar functionality on the server (this is what Angular CDK |
| 68 | +and Material do). |
| 69 | + |
| 70 | +#### Strategy 2: Guards |
| 71 | + |
| 72 | +If you can't inject the proper global value you need from the Angular platform, you can "guard" against |
| 73 | +invocation of browser code, so long as you don't need to access that code on the server. For instance, |
| 74 | +often invocations of the global `window` element are to get window size, or some other visual aspect. |
| 75 | +However, on the server, there is no concept of "screen", and so this functionality is rarely needed. |
| 76 | + |
| 77 | +You may read online and elsewhere that the recommended approach is to use `isPlatformBrowser` or |
| 78 | +`isPlatformServer`. This guidance is **incorrect**. This is because you wind up creating platform-specific |
| 79 | +code branches in your application code. This not only increases the size of your application unnecessarily, |
| 80 | +but it also adds complexity that then has to be maintained. By separating code into separate platform-specific |
| 81 | +modules and implementations, your base code can remain about business logic, and platform-specific exceptions |
| 82 | +are handled as they should be: on a case-by-case abstraction basis. This can be accomplished using Angular's Dependency |
| 83 | +Injection (DI) in order to remove the offending code and drop in a replacement at runtime. Here's an example: |
| 84 | + |
| 85 | +```ts |
| 86 | +// window-service.ts |
| 87 | +import { Injectable } from '@angular/core'; |
| 88 | + |
| 89 | +@Injectable() |
| 90 | +export class WindowService { |
| 91 | + getWidth(): number { |
| 92 | + return window.innerWidth; |
| 93 | + } |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +```ts |
| 98 | +// server-window.service.ts |
| 99 | +import { Injectable } from '@angular/core'; |
| 100 | +import { WindowService } from './window.service'; |
| 101 | + |
| 102 | +@Injectable() |
| 103 | +export class ServerWindowService extends WindowService { |
| 104 | + getWidth(): number { |
| 105 | + return 0; |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +```ts |
| 111 | +// app-server.module.ts |
| 112 | +import {NgModule} from '@angular/core'; |
| 113 | +import {WindowService} from './window.service'; |
| 114 | +import {ServerWindowService} from './server-window.service'; |
| 115 | + |
| 116 | +@NgModule({ |
| 117 | + providers: [{ |
| 118 | + provide: WindowService, |
| 119 | + useClass: ServerWindowService, |
| 120 | + }] |
| 121 | +}) |
| 122 | +``` |
| 123 | + |
| 124 | +If you have a component provided by a third-party that is not Universal-compatible out of the box, |
| 125 | +you can create two separate modules for browser and server (the server module you should already have), |
| 126 | +in addition to your base app module. The base app module will contain all of your platform-agnostic code, |
| 127 | +the browser module will contain all of your browser-specific/server-incompatible code, and vice-versa for |
| 128 | +your server module. In order to avoid editing too much template code, you can create a no-op component |
| 129 | +to drop in for the library component. Here's an example: |
| 130 | + |
| 131 | +```ts |
| 132 | +// example.component.ts |
| 133 | +import { Component } from '@angular/core'; |
| 134 | + |
| 135 | +@Component({ |
| 136 | + selector: 'example-component', |
| 137 | + template: `<library-component></library-component>`, // this is provided by a third-party lib |
| 138 | + // that causes issues rendering on Universal |
| 139 | +}) |
| 140 | +export class ExampleComponent {} |
| 141 | +``` |
| 142 | + |
| 143 | +```ts |
| 144 | +// app.module.ts |
| 145 | +import {NgModule} from '@angular/core'; |
| 146 | +import {ExampleComponent} from './example.component'; |
| 147 | + |
| 148 | +@NgModule({ |
| 149 | + declarations: [ExampleComponent], |
| 150 | +}) |
| 151 | +``` |
| 152 | + |
| 153 | +```ts |
| 154 | +// browser-app.module.ts |
| 155 | +import {NgModule} from '@angular/core'; |
| 156 | +import {LibraryModule} from 'some-lib'; |
| 157 | +import {AppModule} from './app.module'; |
| 158 | + |
| 159 | +@NgModule({ |
| 160 | + imports: [AppModule, LibraryModule], |
| 161 | +}) |
| 162 | +``` |
| 163 | + |
| 164 | +```ts |
| 165 | +// library-shim.component.ts |
| 166 | +import { Component } from '@angular/core'; |
| 167 | + |
| 168 | +@Component({ |
| 169 | + selector: 'library-component', |
| 170 | + template: '', |
| 171 | +}) |
| 172 | +export class LibraryShimComponent {} |
| 173 | +``` |
| 174 | + |
| 175 | +```ts |
| 176 | +// server.app.module.ts |
| 177 | +import { NgModule } from '@angular/core'; |
| 178 | +import { LibraryShimComponent } from './library-shim.component'; |
| 179 | +import { AppModule } from './app.module'; |
| 180 | + |
| 181 | +@NgModule({ |
| 182 | + imports: [AppModule], |
| 183 | + declarations: [LibraryShimComponent], |
| 184 | +}) |
| 185 | +export class ServerAppModule {} |
| 186 | +``` |
| 187 | + |
| 188 | +#### Strategy 3: Shims |
| 189 | + |
| 190 | +If all else fails, and you simply must have access to some sort of browser functionality, you can patch |
| 191 | +the global scope of the server environment to include the globals you need. For instance: |
| 192 | + |
| 193 | +```ts |
| 194 | +// server.ts |
| 195 | +global['window'] = { |
| 196 | + // properties you need implemented here... |
| 197 | +}; |
| 198 | +``` |
| 199 | + |
| 200 | +This can be applied to any undefined element. Please be careful when you do this, as playing with the global |
| 201 | +scope is generally considered an anti-pattern. |
| 202 | + |
| 203 | +> Fun fact: a shim is a patch for functionality that will never be supported on a given platform. A |
| 204 | +> polyfill is a patch for functionality that is planned to be supported, or is supported on newer versions |
| 205 | +
|
| 206 | +## Application is slow, or worse, won't render |
| 207 | + |
| 208 | +The Angular Universal rendering process is straightforward, but just as simply can be blocked or slowed down |
| 209 | +by well-meaning or innocent-looking code. First, some background on the rendering process. When a render |
| 210 | +request is made for platform-server (the Angular Universal platform), a single route navigation is executed. |
| 211 | +When that navigation completes, meaning that all Zone.js macrotasks are completed, the DOM in whatever state |
| 212 | +it's in at that time is returned to the user. |
| 213 | + |
| 214 | +> A Zone.js macrotask is just a JavaScript macrotask that executes in/is patched by Zone.js |
| 215 | +
|
| 216 | +This means that if there is a process, like a microtask, that takes up ticks to complete, or a long-standing |
| 217 | +HTTP request, the rendering process will not complete, or will take longer. Macrotasks include calls to globals |
| 218 | +like `setTimeout` and `setInterval`, and `Observables`. Calling these without cancelling them, or letting them run |
| 219 | +longer than needed on the server could result in suboptimal rendering. |
| 220 | + |
| 221 | +> It may be worth brushing up on the JavaScript event loop and learning the difference between microtasks |
| 222 | +> and macrotasks, if you don't know it already. [Here's](https://javascript.info/event-loop) a good reference. |
| 223 | +
|
| 224 | +## My HTTP, Firebase, WebSocket, etc. won't finish before render! |
| 225 | + |
| 226 | +Similarly to the above section on waiting for macrotasks to complete, the flip-side is that the platform will |
| 227 | +not wait for microtasks to complete before finishing the render. In Angular Universal, we have patched the |
| 228 | +Angular HTTP client to turn it into a macrotask, to ensure that any needed HTTP requests complete for a given |
| 229 | +render. However, this type of patch may not be appropriate for all microtasks, and so it is recommended you use |
| 230 | +your best judgment on how to proceed. You can look at the code reference for how Universal wraps a task to turn |
| 231 | +it into a macrotask, or you can simply opt to change the server behavior of the given tasks. |
0 commit comments