Aspnetcore: Prerendering

Created on 25 Jan 2018  路  32Comments  路  Source: dotnet/aspnetcore

Also known as Server-Side Rendering (SSR)

Done area-blazor enhancement

Most helpful comment

@gulbanana Yup that's the the goal.

All 32 comments

is this issue about the feasibility of server side rendering? in a perfect world, the app assemblies could be netstandard-based, capable of rendering html in an asp.net core server which is then hooked up to the live client code in mono-wasm

@gulbanana Yup that's the the goal.

I would like to get my feet wet on this, but I'm not quite sure how to tackle this.
For example there is the BasicTestApp in the Repo. Which does nothing in Program::Main or nothing of importance.
To make use of prerendering for this App we would need to go about this the same route as with angular and the other spa frameworks. Host nodejs, load mono.wasm and so on. Which in my opinion should not happen.

Probably the BrowserRenderer should not be called directly at all. The purpose of the main method should only be for setting up DI and maybe telling the execution environment which component is the entry component and which selector belongs to it. Which could also be done with an attribute of some sort.

Workflow would look something like this:

  1. Call Main
  2. Discover all entry components
  3. Find all host tags in static index.html
  4. Instantiate components

I started some prototyping on this.
https://github.com/LunicLynx/Blazor/tree/prerendering
For now it only generates the html for the root component.

Missing

  • Return the rendered html in the first request
  • Render / Insert the html into the index.html

This is quick and dirty

Some interesting ideas

  • Provide different implementations for services on the server side.
    This would allow to make api calls directly to the services without making an http request.

Please remember about SEO (prerendering different elements in <head> section for each route). More about this here: https://github.com/aspnet/Blazor/issues/1311#issuecomment-413046895

This should also let us return a 404 from the server if it doesn't match any client-side routes.

In my opinion this feature is super important. Accidentally this is what the SPA world (React/Angular/Vue) means when they say Server Side Rendering. Blazor means something else

I totally agree this is important. It's something we've had planned from the beginning.

We've never intended for Blazor's server-side execution mode to be called "server-side rendering". Some people in the community have used that phrase for it, but I would regard that as a mistake. We will not use the term SSR for server-side Blazor. We recognize that SSR is different.

Regarding SSR, I personally prefer the term "server-side prerendering" or just "prerendering" because it's more clear that the server-side part is just a one-time render, after which client-side code has to take over if it's going to be interactive. Same is true for Angular/React/etc. That's why this issue title is "prerendering". Hope that makes sense!

Yes, it makes sense. I've been trying to find out if you guys are working or at least tracking this feature but any search is stomped by the server-side execution mode results. Luckily a nice guy on reddit pointed me to this issue.

@SteveSandersonMS "prerendering" makes more sense.

Calling it "prerendering" makes sense and this would allow for Blazor to be use for more then just SPA, e.g. MVC. Web Pages.

Is this feature likely to be in the first release of Razor Components in .Net Core 3.0? Would be great to have Blazor apps be crawl-able by Google etc..

@YodasMyDad Yes, I expect so.

@SteveSandersonMS fantastic thanks. Will it be OOTB or something you will have to enable?

It might not be the right place here, so excuse me in advance. I wanted to thank @SteveSandersonMS for his work, basically _being_ the 0.8 percent of all ASP.NET staff working directly on Blazor - as shown in the last ASP.NET community standup. Thanks for making this very enjoyable way of developing web apps possible, a whole bunch of people really like this work.

Thanks @Gaulomatic - that's great to hear! Also I'd add that @rynowak @danroth27 @javiercn from the ASP.NET team have done a lot of the work, plus 20% of our commits are community PRs so you're all to thank too!

@SteveSandersonMS It is great that prerendering is planed for Razor Components v1.0! Unfortunately I'm one of those Blazor fans who prefer client side version. Can we assume or at least have hope that prerendering will work equally well in that version also?

@Andrzej-W That's the goal

I am one of blazor fans that would like more of a mix of server side and client side. I would love to be able to use blazor on asp.net core MVC or Web Page to replace JavaScript. An example of this is a foreach loop that rendering the first 10 item on server side and blazor take over on the client side (e.g. add new item, updates, pagination). I do not really like SPA.

Blazor is a Great and Easy way to create Admin Panels .. It lowers development time to the 1/4 ..
Pre-Rendering is required to be set for front-end SEO,
And we should have the option to enable/disable it per Area (ie disable pre-rendering in Admin Panel and enabling it outside).

@TheGhostFish why would you want to disable prerendering on your Admin Panel. It may not be critical but it will speed up the initial loading.

In admin panel, initial loading time is not an issue compared to the amount of postbacks employees run per hour.. Prerendering every admin request is a waste of cpu/traffic/time.
Current blazor is perfect for heavily used admin panels while prerendering is a must for seo crawled public areas.

@TheGhostFish why would you want the initial load to be slow? Traffic will probably be less, only CPU time might increase but considering that this load will happen rarely I doubt it makes significant difference.

@Eirenarch imho, it's can be usefull at least for debug and test purpose

Sure it can but on Area level can be overkill. Does Blazor have a concept of Area to begin with?

Or when you must use legacy js code and can't render any usefull data without it. User is forced to wait for client side rendering and server side rendering only increase waiting time

It would be very useful to make pre-rendering selectable by any clean way .. by route name or by area or by - maybe not possible - dependency injection. Or even by project level, providing a clean way to run multiple projects calling the same webapi (ie admin panel, front end, ... separate projects).

For anyone who gets blocked by the lack of serverside rendering, this is a class I just wrote to resolve the issue, which was preventing me from being crawled by AdSense.

I have confirmed that it works (using the Google Search Console) and does add some, but not a significant amount overhead to crawling response times. It might be better to do something similar but prerender all of the content and store it in a folder and serve it upon request by a crawler - I dunno.

It is also faster if you change HostURI to the localhost.

It supports all known crawlers inside the JSON data file from https://github.com/monperrus/crawler-user-agents.

It depends on the NuGet package Selenium.WebDriver v3.141.0 and you also must have Chrome installed on the target machine.

Usage goes something like this where you set the MagicWord string to something you expect to exist in the HTML after rendering is complete (such as a header or footer ID found in the rendered Blazor components).

   public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        SeleniumRenderMiddleware.MagicWord = "language_bar";
        app.UseMiddleware<SeleniumRenderMiddleware>();
        app.UseServerSideBlazor<App.Startup>();
    }

```C#
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System;
using System.Collections.Generic;
using Microsoft.JSInterop;
using OpenQA.Selenium.Chrome; //Nuget Package: Selenium.WebDriver v3.141.0
using System.Reflection;

namespace SeleniumRender
{
public class UserAgents
{
public string pattern { get; set; }
public string url { get; set; }
public string[] instances { get; set; }
public string addition_date { get; set; }
public string description { get; set; }
}

public class SeleniumRenderMiddleware
{
    // GOOGLE CHROME MUST BE PRE-INSTALLED ON THE TARGET MACHINE FOR THIS TO WORK.
    private static Dictionary<string, string> s_userAgents = new Dictionary<string, string>();
    private static ChromeOptions s_chromeOptions = new ChromeOptions() { AcceptInsecureCertificates = true };
    private static ChromeDriver s_chromeDriver;
    private static string s_chromeDriverFilename
    {
        get
        {
            if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "chromedriver_win32.zip  ";
            else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "chromedriver_linux64.zip";
            else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "chromedriver_mac64.zip";
            throw new Exception("Are you running OS/2 Warp?!");
        }
    }
    private static string s_chromeVersion = "73.0.3683.20";
    public static string HostURI;
    public static string MagicWord = "0xDEADBEEF";
    private RequestDelegate _next;
    static SeleniumRenderMiddleware()
    {
        string ChromeDriverLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); 

        using (var client = new WebClient())
        {
                if (File.Exists($"{ChromeDriverLocation}\\agents.json") == false)
                    client.DownloadFile("https://raw.githubusercontent.com/monperrus/crawler-user-agents/master/crawler-user-agents.json", $"{ChromeDriverLocation}\\agents.json");
                if (File.Exists($"{ChromeDriverLocation}\\chromedriver.exe") == false)
                {
                    byte[] zipFile = client.DownloadData($"https://chromedriver.storage.googleapis.com/{s_chromeVersion}/{s_chromeDriverFilename}");
                    using (var ms = new MemoryStream(zipFile))
                    {
                        ms.Seek(0, SeekOrigin.Begin);
                        ZipArchive archive = new ZipArchive(ms);
                        archive.ExtractToDirectory(ChromeDriverLocation);
                    }
                }
        }

        foreach (var userAgent in Json.Deserialize<UserAgents[]>(File.ReadAllText($"{ChromeDriverLocation}\\agents.json")))
            foreach (var instance in userAgent.instances)
                s_userAgents.Add(instance, userAgent.pattern);

        s_chromeOptions.AddArgument("headless");
        s_chromeDriver = new ChromeDriver(ChromeDriverLocation, s_chromeOptions);
    }
    public SeleniumRenderMiddleware(RequestDelegate next) => _next = next;
    public async Task Invoke(HttpContext context)
    {
        if (s_userAgents.ContainsKey(context.Request.Headers["User-Agent"]) == false)
            await _next.Invoke(context);
        else
        {
            if (HostURI == null)
                HostURI = (context.Request.IsHttps ? "https://" : "http://") + context.Request.Host.Value;
            s_chromeDriver.Url = HostURI + context.Request.Path;
            s_chromeDriver.Navigate();

            while (s_chromeDriver.PageSource.Contains(MagicWord) == false)
                await Task.Delay(100);

            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync(s_chromeDriver.PageSource);
        }
    }
}

}`

Prerrendering is supported since preview2.

See https://blogs.msdn.microsoft.com/webdev/2019/01/29/aspnet-core-3-preview-2/

The part Integration with MVC Views and Razor Pages

If I'm not mistaken, this can't presently be used in a middle-ware layer to serve crawlers static pages, correct?

_TLDR; +1 for Blazor that does full render on any path from refresh._

Components rendered from pages and views will be prerendered, but are not yet interactive (i.e. clicking the Counter button doesn't do anything in this release).

Non-interactive is not support (imo) it confuses people and probably should have been baked more. I've also been searching for a sample to get this working, to no avail.

I hope this applies to not only components, but mini applications:

@(await Html.RenderComponentAsync<Blazor.Applications.LoginApp>())
@(await Html.RenderComponentAsync<Blazor.Applications.CartApp>())
@(await Html.RenderComponentAsync<Blazor.Applications.ProductsApp>())

Or perhaps better syntax for this usage:

@(await Html.RenderComponentAppAsync<Blazor.Applications.ProductsApp>())

Which would still handle some form of child url routing from where it's rendered.

__Example__
If ProductsApp (RazorComponentApp) has routes "/" & "/specials"
If this was placed on a "/page" mvc/razor route "/page/specials" would still resolve, prerender and serve from the ProductsApp component from where it was called in the application. _(Hopefully the idea is articulated here without too much detail)_

The lack of ability to have a 100% rendered page on direct browse to /counter is what I believe to be the limiting factor on most corporate/commercial projects. Especially ones that do not want either the full framework or entire application sent downstream. _(Not everyone is making a mobile spa or electron app)_

Of course, if a Blazor app could do full prerendering, I think all this is moot and we would convert our apps to full on server Blazor, and I second the suggestion that some areas do not need to be prerendered and would love <Counter @prerender/> and/or @page(prerender: true) "/counter" or similar for optimization.

_My 2c, is that my apps are far from SPA for various reasons, both functional and SEO are huge considerations. I think this is an amazing use of C# that I am looking forward to see expanded. This is what <asp:UpdatePanel> should have been all along, with the ability to route._

Also, to those asking why you would NOT prerender a portion on a prerendered whole: You may need to have calls to external services you don't want the server side making... calls to weather.json for example, the server would prerender the site, SEO is all good, and then collects data from potentially _other_ servers/services.

__Updated 2/19/19__
_Hold the phone!_

https://youtu.be/Qe8UW5543-s?t=2850

I need to revisit this. I couldn't get it to work, and this demo seems to be exactly what is needed (with the exception of the interactive pieces, which is a big part).

Link should start at 47:30

This has been done as part of https://github.com/aspnet/AspNetCore/pull/7770
There are some minor improvements left that will be tracked separately.

Was this page helpful?
0 / 5 - 0 ratings