Skip to content

Commit d048b19

Browse files
committed
Add more details in tutorial of Server-Side-Rendering
1 parent d971a2b commit d048b19

File tree

4 files changed

+99
-24
lines changed

4 files changed

+99
-24
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ Fable bindings and helpers for React projects
44

55
## Documents
66

7-
### [Server-Side Rendering](docs/server-side-rendering.md):
8-
9-
A **Pure F#** solution for Server-Side Rendering, **No NodeJS Required!**
7+
* [Server-Side Rendering tutorial](docs/server-side-rendering.md): A **Pure F#** solution for SSR, **No NodeJS Required!**
108

119

1210

Samples/SSRSample/src/Client/Bench.fs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ let jsRenderBench () =
2323
if not isNode then () else
2424

2525
let renderToString: ReactElement -> string = importMember "react-dom/server"
26-
let mutable len = 10000
27-
console.log(renderToString (view initState ignore))
28-
console.time("render")
29-
while len > 0 do
26+
let mutable times = 10000
27+
let label = sprintf "render %d times in nodejs" times
28+
console.time(label)
29+
while times > 0 do
3030
renderToString (view initState ignore) |> ignore
31-
len <- len - 1
32-
console.timeEnd("render")
31+
times <- times - 1
32+
console.timeEnd(label)
3333

3434
jsRenderBench ()

Samples/SSRSample/src/Server/Server.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ let bench () =
3636
Fable.Helpers.ReactServer.renderToString(Client.View.view initState ignore)
3737
|> ignore
3838
watch.Stop()
39-
printfn "render %d times: %dms" times watch.ElapsedMilliseconds
39+
printfn "render %d times in dotnet core: %dms" times watch.ElapsedMilliseconds
4040

4141
bench ()
4242

docs/server-side-rendering.md

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,58 @@
1-
# Five steps to enable Server-Side Rendering in your Elmish + DotNet App!
1+
# Five steps to enable Server-Side Rendering in your [Elmish](https://github.com/fable-elmish/elmish) + [DotNet Core](https://github.com/dotnet/core) App!
22

3-
[SSR Sample App of SAFE-stack template is available!](https://github.com/fable-compiler/fable-react/tree/master/Samples/SSRSample)
3+
> [SSR Sample App](https://github.com/fable-compiler/fable-react/tree/master/Samples/SSRSample) based on [SAFE-Stack](https://github.com/SAFE-Stack/SAFE-BookStore) template is available!
4+
5+
## Introduction
6+
7+
### What is Server-Side Rendering (SSR) ?
8+
9+
Commonly speaking SSR means the majority of your app's code can run on both the server and the client, it is also as known as "isomorphic app" or "universal app". In React, you can render your components to html on the server side (usually a nodejs server) by `ReactDOMServer.renderToString`, reuse the server-rendered html and bind events on the client side by `React.hydrate`.
10+
11+
#### Props
12+
13+
* Better SEO, as the search engine crawlers will directly see the fully rendered page.
14+
* Faster time-to-content, especially on slow internet or slow devices.
15+
16+
#### Cons
17+
18+
* Development constraints, browser-specific code need add compile directives to ignore in the server.
19+
* More involved build setup and deployment requirements.
20+
* More server-side load.
21+
22+
#### Conclusions
23+
24+
While SSR looks pretty cool, it still adds more complexity to your app, and increases server-side load. But it could be really helpful in some cases like solving SEO issue in SPAs, improving time-to-content of mobile sites, etc.
25+
26+
### Server-Side Rendering in fable-react
27+
28+
fable-react's SSR approach is a little different from those you see on the network, it is a **Pure F#** approach. It means you can render your elmish's view function directly on dotnet core, with all benefits of dotnet core runtime!
29+
30+
There are lots of articles about comparing dotnet core and nodejs, I will only mention two main differences between F#/dotnet core and nodejs in SSR:
31+
32+
* F# is a compiled language, which means it's generally considered faster then a dynamic language, like js.
33+
* Nodejs's single thread, event-driven, non-blocking I/O model works well in most web sites, but it is not good at CPU intensive tasks, including html rendering. Usually we need to run multi nodejs instances to take the advantage of multi-core systems. DotNet support non-blocking I/O (and `async/await` sugar), too. But the awesome part is that it also has pretty good support for multi-thread programming.
34+
35+
In a simple test in my local macbook, rendering on dotnet core is about ~2x faster then nodejs (with ReactDOMServer.renderToString + NODE_ENV=production). You can find more detail in the bottom of this page.
36+
37+
In a word, with this approach, you can not only get a better performance then nodejs, but also don't need the complexity of running and maintaining nodejs instances on your server!
38+
39+
Here is a list of Fable.Helpers.React API that support server-side rendering:
40+
41+
* HTML/CSS/SVG DSL function/unions, like `div`, `input`, `Style`, `Display`, `svg`, etc.
42+
* str/ofString/ofInt/ofFloat
43+
* ofOption/ofArray/ofList
44+
* fragment
45+
* ofType
46+
* ofFunction
47+
48+
These don't support, but you can wrap it by `Fable.Helpers.Isomorphic.isomorphicView` to skip or render a placeholder on the server:
49+
50+
* ofImport
451

552
## Step 1: Reorganize your source files
653

754
Separate all your elmish view and types to standalone files, like this:
855

9-
```F#
1056
pages
1157
|-- Home
1258
|-- View.fs // contains view function.
@@ -19,7 +65,7 @@ View.fs and Types.fs will be shared between client and server.
1965
2066
## Step 2. Make sure shared files can be executed on the server side
2167
22-
Some code that works in Fable might throw a run time exception when executed on dotnet, so we should be careful with unsafe type casting and add compiler directives to remove some code if necessary.
68+
Some code that works in Fable might throw a runtime exception on dotnet core, we should be careful with unsafe type casting and add compiler directives to remove some code if necessary.
2369
2470
Here are some hints about doing this:
2571
@@ -40,9 +86,9 @@ Here are some hints about doing this:
4086

4187
### 2. Make sure your browser/js code won't be executed on the server side
4288

43-
One big challenge of sharing code between client and server is that server side has different API environment than client side. In this respect Fable + dotnet's SSR is not much different than nodejs, except in dotnet you should not only prevent browser's API call, but also js.
89+
One big challenge of sharing code between client and server is that the server side has different API environment with client side. In this respect Fable + dotnet core's SSR is not much different than nodejs, except on dotnet core you should not only prevent browser's API call, but also js.
4490

45-
Thanks for Fable Compiler's `FABLE_COMPILER` directive, we can easly distinguish client environment and server environment and execute different code in each environment:
91+
Thanks for Fable Compiler's `FABLE_COMPILER` directive, we can easily distinguish it's running on client or server and execute different code in different environment:
4692

4793
```#F
4894
#if FABLE_COMPILER
@@ -52,7 +98,7 @@ Thanks for Fable Compiler's `FABLE_COMPILER` directive, we can easly distinguish
5298
#endif
5399
```
54100

55-
We also provice a help function in `Fable.Helpers.Isomorphic` of this, the definition is:
101+
We also provide a help function in `Fable.Helpers.Isomorphic`, the definition is:
56102

57103
```F#
58104
let inline isomorphicExec clientFn serverFn input =
@@ -71,7 +117,7 @@ open Fable.Import.JS
71117
open Fable.Helpers.Isomorphic
72118
open Fable.Import.Browser
73119

74-
// example code to make your document's title has marquee effect
120+
// example code to add marquee effect to your document's title
75121
-window.setInterval(
76122
- fun () ->
77123
- document.title <- document.title.[1..len - 1] + document.title.[0..0],
@@ -91,7 +137,7 @@ open Fable.Import.Browser
91137

92138
### 3. Add a placeholder for components that cannot been rendered on the server side, like js native components.
93139

94-
In `Fable.Helpers.Isomorphic` we also implemented a help function to render a placeholder element for components that cannot been rendered on the server side, this function will also help [React.hydrate](https://reactjs.org/docs/react-dom.html#hydrate) to understand the differences between htmls rendered by client and server, so React won't treat it as a mistake and warn about it.
140+
In `Fable.Helpers.Isomorphic` we also implemented a help function (`isomorphicView`) to render a placeholder element for components that cannot be rendered on the server side, this function will also help [React.hydrate](https://reactjs.org/docs/react-dom.html#hydrate) to understand the differences between htmls rendered by client and server, so React won't treat it as a mistake and warn about it.
95141

96142
```diff
97143
open Fable.Core
@@ -116,9 +162,9 @@ let jsComp (props: JsCompProps) =
116162
+isomorphicView jsComp jsCompServer { text="I'm rendered by a js Component!" }
117163
```
118164

119-
## Step 3. Create your init state on the server side.
165+
## Step 3. Create your initial state on the server side.
120166

121-
On the server side, you could create routes like normal MVC app, just make sure the model passed to server rendering function is exactly match the model on the client side in current route.
167+
On the server side, you can create routes like normal MVC app, just make sure the model passed to server-side rendering function is exactly match the model on the client side in current route.
122168

123169
Here is an example:
124170

@@ -165,8 +211,8 @@ let renderHtml () =
165211

166212
## Step 4. Update your elmish app's init function
167213

168-
1. Init your elmish app by state printed in the HTML.
169-
2. Remove init commands that still fetch data which already printed in the HTML.
214+
1. Initialize your elmish app by state printed in the HTML.
215+
2. Remove initial commands that fetch state which already included in the HTML.
170216

171217
e.g.
172218

@@ -199,7 +245,7 @@ Program.mkProgram init update view
199245
|> Program.run
200246
```
201247

202-
Now enjoy! If you find bugs or just need some help, please create an issue and let us know.
248+
Now enjoy! If you find bugs or just need some help, please create an issue and let us know, thanks!
203249

204250
## Try the sample app
205251

@@ -208,3 +254,34 @@ git clone https://github.com/fable-compiler/fable-react.git
208254
cd ./fable-react/Samples/SSRSample/
209255
./build.sh run # or ./build.cmd run on windows
210256
```
257+
258+
## Run simple benchmark test in sample app
259+
260+
The result of dotnet core is already printed in console when your server started, here are some commands to run benchmark of ReactDOMServer on nodejs.
261+
262+
```sh
263+
cd ./Samples/SSRSample/src/Client
264+
dotnet fable npm-run buildClientLib
265+
NODE_ENV=production node ./bin/lib/Bench.js
266+
```
267+
268+
### Benchmark result in my laptop (MacBook Pro with 2.7 GHz Intel Core i5, 16 GB 1867 MHz DDR3):
269+
270+
```sh
271+
272+
# SSR on dotnet core with Debug mode
273+
# dir: fable-react/Samples/SSRSample/src/Server
274+
$ dotnet run
275+
render 10000 times in dotnet core: 3731ms
276+
277+
# SSR on dotnet core with Release mode
278+
# dir: fable-react/Samples/SSRSample/src/Server
279+
$ dotnet run --configuration Release
280+
render 10000 times in dotnet core: 2698ms
281+
282+
# SSR on nodejs with NODE_ENV=production
283+
# dir: fable-react/Samples/SSRSample/src/Client
284+
$ NODE_ENV=production node ./bin/lib/Bench.js
285+
render 10000 times in nodejs: 5782.382ms
286+
287+
```

0 commit comments

Comments
 (0)