angular-cli: index.html formatting breaks Universal SSR

Versions

Angular CLI: 1.6.1
Node: 8.9.1
OS: darwin x64
Angular: 5.1.1
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, platform-server, router
... service-worker

@angular/cli: 1.6.1
@angular-devkit/build-optimizer: 0.0.36
@angular-devkit/core: 0.0.22
@angular-devkit/schematics: 0.0.42
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.9.1
@schematics/angular: 0.1.11
typescript: 2.4.2
webpack-bundle-analyzer: 2.9.1
webpack: 3.10.0

Repro steps

Build the app

Observed behavior

After building the app and starting the express app and I can see I not getting SSR content. However if I open the compiled index.html and save it it Visual Code so that the html is formatted again the problem goes away. This suggests that the build process is somehow formatting the html in a way that’s causing the issue, but I don’t get any errors.

Also the other odd thing is if I removed the Service Worker I don’t get the issue so I initially though that was the problem as reported here https://github.com/angular/angular/issues/20890

Desired behavior

Be able to include build app and have SSR.

I’m just very confused as to how the combination of the formatted html and service worker can be preventing ssr working.

Mention any other details that might be useful (optional)

I’ve tried removing all additions to my index.html to make sure nothing I’ve added causes the issue. I’ve only recently started having this issue but can’t be sure of which version.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 1
  • Comments: 49 (7 by maintainers)

Most upvoted comments

@gkalpak :

just removing the index.html entry from ngsw.json (as was suggested in #8872 (comment)) will break offline afaict.

you are right: you have to remove it form the hashtable. bec the ngsw code will check the hashvalue of the served files with the hashtable, and if this hashes are not equal (which is not possible bec how should you know at compilation time, what the hash of a dynamic prerendered file looks like). the sw will not cache this index.html and it will not work offline. … so you have to force the sw to accept and cache the prerendered index.html (thats what i mean).

Regarding your approach to cache prerendered index.html: Unless I am misunderstanding something, this will not work correctly when you have routes, because the same index.html will be rendered for all of them (which look like navigation requests to the SW). In the best case scenario, you will get a flash of irrelevant content (which is way worse imo than a delay of meaningful content 😁).

and yes thats the next point i didnt mentioned yet. and there is no workaround for it ^^ theoretically the sw need to refresh the cached index.html on every route change … which leads to other problems.

So all over all i think we both agree on not doing any workarounds and use the default mechanism, there is a reason why it acts like it does. To change this behaviour a completly different implementation is required. -> which leads to other disadvantages …

in my opinion closing this issue is pretty good. there is no need to discuss a scenario (workaround) nobody should use 😃

as i said, i just wanted to clearify why things work like they do with the default implementation.

cheers

But I am sure you understand that ssr should refresh index content on every inner navigation, but if only first navigation is cached so it is not SEO friendly because no matter where bot navigates internally it gets same cached index? That’s why SW is not compatible with SSR i think. Or?

No, I don’t think that removing index.html from the generated ngsw.json would allow the app to work offline. I definitely do not recommend that.

As has been mentioned in some answers above, SSR works as expected on the first request (before the SW is activated). Once the SW takes over, the non-prerendered index.html is served, but that is OK, since it is served by the SW and doesn’t require a round-trip to the server. (Offline mode also works fine in that case.)

This is by design: SSR speeds up the requests where no SW is in effect (including first-time visits, SEO, etc) and SW speeds up subsequent requests (and enables offline mode) by serving the cached files and scripts.

I am going to close this, since everything works as expected. Note: There are still issues with using app-shell. This is tracked in #8794.

That hint re: deleting the index.html hash from the ngsw-manifest output was worth gold, @Priemar !! Thank you so much.

Now I’ve got Universal & Service Worker working together in harmony ❤️

Really should be a proper way to do this though.

And thats the point what i mentioned in my first answer. The generated ngsw-serviceworker caches index.html. If im reloading the page, always this index.html is served from cache.

You can check if swprecache acts like ngsw. Remove all serviceworkers and clear the cache. Then navigate to your app. If you check the network traffic, you should see the initial page (url) is from the server and prerendered. -> thats should work.

in the meantime the serviceworker prefetches all defined resources. Please check the

index.html

this is your fallback page. -> this index html should also prerendered.

If you reload the app, no matter which route you take, the network traffic will tell you if the route is taken from cache. Which i expect is the prerendered index.html

If thats the case you have exactly the same behv. i got with ngsw. In my opinion SSR for those service-worker (ngsw, maybe swprecache) implementations is no implemented as expected.

Its working for the inital load. (thats works) But after page changes, the serviceworker should fetch the actual route from the server and caches this route. If you reload the page now, it should return the last cached route. Thats what i expected.

Theoretically its not difficult to implement a serviceworker which acts like this. But rewriting the whole generation logic and keep it uptodate is a lot of work.

Im not sure but i think swprecache allow you to write plugins. I dont know how this plugin mechanism works, but maybe you can write a plugin which fetches the last page after navigating in your app.

If you dont want to rewrite the serviceworker, i recommend using an appshell. I think thats a good mix of SSR and serviceworker, the page feels pretty fast, even not all informations are visible on refresh.

My case was a bit more tricky i needed to deliver different app-shells. One for the logged-in user and one for the not logged in once. So i had to give the serviceworker prefetch the information if the user is logged in or not (which is also not supported in the actual service worker implementations)

If i get the chance to get in contact with one of the angular core developers, i would discuss this and maybe we find a good solution on the base system. If there are not more ppl which need this behaviour to SSR PWAs i think this feature will not be available soon ^^ and the only solution is to write your own service-worker.

but ill keep you up to date, if there is something new. or if i found another solution for this.

sorry for the bad news

Hey guys,

after investigating some hours, i want to share my results. (feel free to correct me if im wrong)

my setup:
angular cli 6.0.1

  • service worker
  • ssr (angular universal)
  • i used the smallest setup for ngsw-config.json
{
  "index": "/index.html",
  "assetGroups": [{
    "name": "app",
    "installMode": "prefetch",
    "resources": {
      "files": [
        "/favicon.ico",
        "/index.html",
        "/manifest.json",
        "/*.css",
        "/*.js"
      ]
    }
  }, {
    "name": "assets",
    "installMode": "lazy",
    "updateMode": "prefetch",
    "resources": {
      "files": [
        "/assets/**"
      ]
    }
  }]
}

FYI: i think formatting the index.html is not the reason why the ssr, etc. doesnt work as expected.

How it works under the hood:

Navigate to the site for the first time: (i.e.: http://localhost:4200) this response is the SSR rendered version of the index.html. (so far so good) the SSR rendered index.html will be shown.

the service worker gets installed, and starts to load the resources (in the meantime) (def. in ngsw-config.json) in our example the index.html. -> this index.html will be cached. (if the site gets reloaded or you are offline, the serviceworker will return this index.html. Thats what we expect).

after analysing the network traffic, i saw that the index.html (resource) which is loaded by the service worker is never prerendered. so i watched the server code (here is the important part)

app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

app.get('*', (req, res) => {
  res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});

in our case when the serviceworker loads the index.html he the server route for returning the static file from our dist/browser folder is used. _its like calling http://localhost:4200/index.html_ which means the cached index.html is never ssr. its always the static file from our dist/browser folder.

After reloading the site the service worker will return the cached index.html and thats it. using an app-shell would help in that case. bec. the app-shell is compiled and added in our static index.html. (maybe thats a solution for some of you), but in my case i definitly want to use ssr.

i tried to modify the server source, to return also a rendered version of the index.html. instead of returning the static version. I added

app.get('/index.html', (req, res) => {
  res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
....
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

before the static file path. Now the result was as expected. The first call worked (ssr page). service worker loaded the prerendered index.html. (that looked perfect)

sadly there was the next problem incoming: when i tried turned the network off. the site was not working anymore. (but thats one of the most important things for my case)

to figure out why took me quite a while.

the problem here was: the serviceworker loaded the ssr index.html. but if you take a look at angular-cli dist folder (there is a file ngsw.json) which contains a hashtable for all static resources. like our index.html and this hash is based on the static file in the dist folder.

if we return an ssr index.html -> its content look different (obvi.) that results in a different hash. but the service-worker compares the hash from the ngsw.json hashtable and the response (index.html) if its hashes doesnt match he will do another call index.html?ngsw-cache-bust (to load the index.html again) and if this hash is also different (which is obvi.) the index.html will never get cached.

-> and thats the crux here. a solution would be:

  • manipulating the server.ts as i did. to return a ssr index.html version
  • and removing the index.html hash from ngsw.json (but this file is generated and i didnt found a proper solution todo this (not sure if its a good idea todo this) that looks like a dirty workaround, but it should work)

Resumee: (but thats only my personal opinion)

SSR combined with PWA is actually only worth for the first initial call of the site. -> bec. its fast and got prerendered. After reloading, the SSR has absolutly no effect. except you want to implement the solution above.

Whats also important to keep in mind. if you are using a proxy you have to disable the caching for the index.html -> otherwise ssr will not work (or if you you render with user specific content, it will be a security problem… cached user specific data…)

Sorry for such a long post, but i didnt found a good answer searching the web. Maybe it helped someone. (if anything is not clear feel free to contact me)