Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prerendering #5464

Closed
danroth27 opened this issue Jan 25, 2018 · 32 comments
Closed

Prerendering #5464

danroth27 opened this issue Jan 25, 2018 · 32 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one

Comments

@danroth27
Copy link
Member

Also known as Server-Side Rendering (SSR)

@gulbanana
Copy link

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

@danroth27
Copy link
Member Author

@gulbanana Yup that's the the goal.

@LunicLynx
Copy link
Contributor

LunicLynx commented Mar 7, 2018

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

@LunicLynx
Copy link
Contributor

LunicLynx commented Mar 11, 2018

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.

@Andrzej-W
Copy link

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

@SteveSandersonMS
Copy link
Member

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

@Eirenarch
Copy link

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

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Oct 3, 2018

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!

@Eirenarch
Copy link

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.

@mrpmorris
Copy link

@SteveSandersonMS "prerendering" makes more sense.

@A51UK
Copy link

A51UK commented Oct 10, 2018

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

@YodasMyDad
Copy link

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..

@SteveSandersonMS
Copy link
Member

@YodasMyDad Yes, I expect so.

@YodasMyDad
Copy link

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

@Gaulomatic
Copy link

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.

@SteveSandersonMS
Copy link
Member

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!

@Andrzej-W
Copy link

@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?

@danroth27
Copy link
Member Author

@Andrzej-W That's the goal

@A51UK
Copy link

A51UK commented Nov 7, 2018

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.

@TheGhostFish
Copy link

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).

@Eirenarch
Copy link

@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.

@TheGhostFish
Copy link

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.

@Eirenarch
Copy link

@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.

@proff
Copy link
Contributor

proff commented Nov 10, 2018

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

@Eirenarch
Copy link

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

@proff
Copy link
Contributor

proff commented Nov 10, 2018

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

@TheGhostFish
Copy link

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).

@aspnet-hello aspnet-hello transferred this issue from dotnet/blazor Dec 17, 2018
@aspnet-hello aspnet-hello added this to the Backlog milestone Dec 17, 2018
@aspnet-hello aspnet-hello added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Dec 17, 2018
@aspnet-hello aspnet-hello added enhancement This issue represents an ask for new feature or an enhancement to an existing one area-blazor Includes: Blazor, Razor Components labels Dec 17, 2018
@danroth27 danroth27 modified the milestones: Backlog, 3.0.0-preview2 Jan 4, 2019
@plasticalligator
Copy link

plasticalligator commented Feb 11, 2019

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>();
    }
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);
            }
        }
    }
}`

@javiercn
Copy link
Member

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

@plasticalligator
Copy link

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

@ElectricHavoc
Copy link

ElectricHavoc commented Feb 17, 2019

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

@javiercn javiercn added Done This issue has been fixed and removed 2 - Working labels Feb 22, 2019
@javiercn
Copy link
Member

This has been done as part of #7770
There are some minor improvements left that will be tracked separately.

@rynowak rynowak mentioned this issue Mar 4, 2019
56 tasks
@mkArtakMSFT mkArtakMSFT removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels May 9, 2019
@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one
Projects
None yet
Development

No branches or pull requests