render.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. //! A shared pool of renderers for efficient server side rendering.
  2. use crate::streaming::StreamingRenderer;
  3. use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
  4. use dioxus_ssr::{
  5. incremental::{CachedRender, RenderFreshness},
  6. Renderer,
  7. };
  8. use futures_channel::mpsc::Sender;
  9. use futures_util::{Stream, StreamExt};
  10. use std::sync::RwLock;
  11. use std::{collections::HashMap, future::Future};
  12. use std::{fmt::Write, sync::Arc};
  13. use tokio::task::JoinHandle;
  14. use crate::prelude::*;
  15. use dioxus_lib::prelude::*;
  16. fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
  17. where
  18. Fut: Future + 'static,
  19. Fut::Output: Send + 'static,
  20. {
  21. #[cfg(not(target_arch = "wasm32"))]
  22. {
  23. use tokio_util::task::LocalPoolHandle;
  24. static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
  25. let pool = TASK_POOL.get_or_init(|| {
  26. let threads = std::thread::available_parallelism()
  27. .unwrap_or(std::num::NonZeroUsize::new(1).unwrap());
  28. LocalPoolHandle::new(threads.into())
  29. });
  30. pool.spawn_pinned(f)
  31. }
  32. #[cfg(target_arch = "wasm32")]
  33. {
  34. tokio::task::spawn_local(f())
  35. }
  36. }
  37. struct SsrRendererPool {
  38. renderers: RwLock<Vec<Renderer>>,
  39. incremental_cache: Option<RwLock<dioxus_ssr::incremental::IncrementalRenderer>>,
  40. }
  41. impl SsrRendererPool {
  42. fn new(
  43. initial_size: usize,
  44. incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
  45. ) -> Self {
  46. let renderers = RwLock::new((0..initial_size).map(|_| pre_renderer()).collect());
  47. Self {
  48. renderers,
  49. incremental_cache: incremental.map(|cache| RwLock::new(cache.build())),
  50. }
  51. }
  52. fn check_cached_route(
  53. &self,
  54. route: &str,
  55. render_into: &mut Sender<Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
  56. ) -> Option<RenderFreshness> {
  57. if let Some(incremental) = &self.incremental_cache {
  58. if let Ok(mut incremental) = incremental.write() {
  59. match incremental.get(route) {
  60. Ok(Some(cached_render)) => {
  61. let CachedRender {
  62. freshness,
  63. response,
  64. ..
  65. } = cached_render;
  66. _ = render_into.start_send(String::from_utf8(response.to_vec()).map_err(
  67. |err| {
  68. dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
  69. err,
  70. ))
  71. },
  72. ));
  73. return Some(freshness);
  74. }
  75. Err(e) => {
  76. tracing::error!(
  77. "Failed to get route \"{route}\" from incremental cache: {e}"
  78. );
  79. }
  80. _ => {}
  81. }
  82. }
  83. }
  84. None
  85. }
  86. async fn render_to(
  87. self: Arc<Self>,
  88. cfg: &ServeConfig,
  89. route: String,
  90. virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
  91. server_context: &DioxusServerContext,
  92. ) -> Result<
  93. (
  94. RenderFreshness,
  95. impl Stream<Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
  96. ),
  97. dioxus_ssr::incremental::IncrementalRendererError,
  98. > {
  99. struct ReceiverWithDrop {
  100. receiver: futures_channel::mpsc::Receiver<
  101. Result<String, dioxus_ssr::incremental::IncrementalRendererError>,
  102. >,
  103. cancel_task: Option<tokio::task::JoinHandle<()>>,
  104. }
  105. impl Stream for ReceiverWithDrop {
  106. type Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>;
  107. fn poll_next(
  108. mut self: std::pin::Pin<&mut Self>,
  109. cx: &mut std::task::Context<'_>,
  110. ) -> std::task::Poll<Option<Self::Item>> {
  111. self.receiver.poll_next_unpin(cx)
  112. }
  113. }
  114. // When we drop the stream, we need to cancel the task that is feeding values to the stream
  115. impl Drop for ReceiverWithDrop {
  116. fn drop(&mut self) {
  117. if let Some(cancel_task) = self.cancel_task.take() {
  118. cancel_task.abort();
  119. }
  120. }
  121. }
  122. let (mut into, rx) = futures_channel::mpsc::channel::<
  123. Result<String, dioxus_ssr::incremental::IncrementalRendererError>,
  124. >(1000);
  125. // before we even spawn anything, we can check synchronously if we have the route cached
  126. if let Some(freshness) = self.check_cached_route(&route, &mut into) {
  127. return Ok((
  128. freshness,
  129. ReceiverWithDrop {
  130. receiver: rx,
  131. cancel_task: None,
  132. },
  133. ));
  134. }
  135. let wrapper = FullstackHTMLTemplate { cfg: cfg.clone() };
  136. let server_context = server_context.clone();
  137. let mut renderer = self
  138. .renderers
  139. .write()
  140. .unwrap()
  141. .pop()
  142. .unwrap_or_else(pre_renderer);
  143. let myself = self.clone();
  144. let join_handle = spawn_platform(move || async move {
  145. let mut virtual_dom = virtual_dom_factory();
  146. let mut pre_body = String::new();
  147. if let Err(err) = wrapper.render_before_body(&mut pre_body) {
  148. _ = into.start_send(Err(err));
  149. return;
  150. }
  151. if let Err(err) = write!(&mut pre_body, "<script>{INITIALIZE_STREAMING_JS}</script>") {
  152. _ = into.start_send(Err(
  153. dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
  154. ));
  155. return;
  156. }
  157. let stream = Arc::new(StreamingRenderer::new(pre_body, into));
  158. let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
  159. renderer.pre_render = true;
  160. {
  161. let scope_to_mount_mapping = scope_to_mount_mapping.clone();
  162. let stream = stream.clone();
  163. renderer.set_render_components(move |renderer, to, vdom, scope| {
  164. let is_suspense_boundary = vdom
  165. .get_scope(scope)
  166. .and_then(|s| SuspenseBoundaryProps::downcast_from_scope(s))
  167. .filter(|s| s.suspended())
  168. .is_some();
  169. if is_suspense_boundary {
  170. let mount = stream.render_placeholder(
  171. |to| renderer.render_scope(to, vdom, scope),
  172. &mut *to,
  173. )?;
  174. scope_to_mount_mapping.write().unwrap().insert(scope, mount);
  175. } else {
  176. renderer.render_scope(to, vdom, scope)?
  177. }
  178. Ok(())
  179. });
  180. }
  181. macro_rules! throw_error {
  182. ($e:expr) => {
  183. stream.close_with_error($e);
  184. return;
  185. };
  186. }
  187. // poll the future, which may call server_context()
  188. tracing::info!("Rebuilding vdom");
  189. with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
  190. // Render the initial frame with loading placeholders
  191. let mut initial_frame = renderer.render(&virtual_dom);
  192. // Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
  193. // Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
  194. let resolved_data = serialize_server_data(&virtual_dom, ScopeId::ROOT);
  195. initial_frame.push_str(&format!(
  196. r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
  197. ));
  198. // Along with the initial frame, we render the html after the main element, but before the body tag closes. This should include the script that starts loading the wasm bundle.
  199. if let Err(err) = wrapper.render_after_main(&mut initial_frame) {
  200. throw_error!(err);
  201. }
  202. stream.render(initial_frame);
  203. // After the initial render, we need to resolve suspense
  204. while virtual_dom.suspended_tasks_remaining() {
  205. ProvideServerContext::new(
  206. virtual_dom.wait_for_suspense_work(),
  207. server_context.clone(),
  208. )
  209. .await;
  210. let resolved_suspense_nodes = ProvideServerContext::new(
  211. virtual_dom.render_suspense_immediate(),
  212. server_context.clone(),
  213. )
  214. .await;
  215. // Just rerender the resolved nodes
  216. for scope in resolved_suspense_nodes {
  217. let mount = {
  218. let mut lock = scope_to_mount_mapping.write().unwrap();
  219. lock.remove(&scope).unwrap()
  220. };
  221. let mut resolved_chunk = String::new();
  222. // After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
  223. let render_suspense = |into: &mut String| {
  224. renderer.reset_hydration();
  225. renderer.render_scope(into, &virtual_dom, scope)
  226. };
  227. let resolved_data = serialize_server_data(&virtual_dom, scope);
  228. if let Err(err) = stream.replace_placeholder(
  229. mount,
  230. render_suspense,
  231. resolved_data,
  232. &mut resolved_chunk,
  233. ) {
  234. throw_error!(
  235. dioxus_ssr::incremental::IncrementalRendererError::RenderError(err)
  236. );
  237. }
  238. stream.render(resolved_chunk);
  239. }
  240. }
  241. tracing::info!("Suspense resolved");
  242. // After suspense is done, we render the html after the body
  243. let mut post_streaming = String::new();
  244. if let Err(err) = wrapper.render_after_body(&mut post_streaming) {
  245. throw_error!(err);
  246. }
  247. // If incremental rendering is enabled, add the new render to the cache without the streaming bits
  248. if let Some(incremental) = &self.incremental_cache {
  249. let mut cached_render = String::new();
  250. if let Err(err) = wrapper.render_before_body(&mut cached_render) {
  251. throw_error!(err);
  252. }
  253. cached_render.push_str(&post_streaming);
  254. if let Ok(mut incremental) = incremental.write() {
  255. let _ = incremental.cache(route, cached_render);
  256. }
  257. }
  258. stream.render(post_streaming);
  259. renderer.reset_render_components();
  260. myself.renderers.write().unwrap().push(renderer);
  261. });
  262. Ok((
  263. RenderFreshness::now(None),
  264. ReceiverWithDrop {
  265. receiver: rx,
  266. cancel_task: Some(join_handle),
  267. },
  268. ))
  269. }
  270. }
  271. fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> String {
  272. // After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
  273. // Extract any data we serialized for hydration (from server futures)
  274. let html_data =
  275. crate::html_storage::HTMLData::extract_from_suspense_boundary(virtual_dom, scope);
  276. // serialize the server state into a base64 string
  277. html_data.serialized()
  278. }
  279. /// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
  280. #[derive(Clone)]
  281. pub struct SSRState {
  282. // We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
  283. renderers: Arc<SsrRendererPool>,
  284. }
  285. impl SSRState {
  286. /// Create a new [`SSRState`].
  287. pub fn new(cfg: &ServeConfig) -> Self {
  288. Self {
  289. renderers: Arc::new(SsrRendererPool::new(4, cfg.incremental.clone())),
  290. }
  291. }
  292. /// Render the application to HTML.
  293. pub async fn render<'a>(
  294. &'a self,
  295. route: String,
  296. cfg: &'a ServeConfig,
  297. virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
  298. server_context: &'a DioxusServerContext,
  299. ) -> Result<
  300. (
  301. RenderFreshness,
  302. impl Stream<Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
  303. ),
  304. dioxus_ssr::incremental::IncrementalRendererError,
  305. > {
  306. self.renderers
  307. .clone()
  308. .render_to(cfg, route, virtual_dom_factory, server_context)
  309. .await
  310. }
  311. }
  312. /// The template that wraps the body of the HTML for a fullstack page. This template contains the data needed to hydrate server functions that were run on the server.
  313. #[derive(Default)]
  314. pub struct FullstackHTMLTemplate {
  315. cfg: ServeConfig,
  316. }
  317. impl FullstackHTMLTemplate {
  318. /// Create a new [`FullstackHTMLTemplate`].
  319. pub fn new(cfg: &ServeConfig) -> Self {
  320. Self { cfg: cfg.clone() }
  321. }
  322. }
  323. impl FullstackHTMLTemplate {
  324. /// Render any content before the body of the page.
  325. pub fn render_before_body<R: std::fmt::Write>(
  326. &self,
  327. to: &mut R,
  328. ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
  329. let ServeConfig { index, .. } = &self.cfg;
  330. to.write_str(&index.pre_main)?;
  331. Ok(())
  332. }
  333. /// Render all content after the main element of the page.
  334. pub fn render_after_main<R: std::fmt::Write>(
  335. &self,
  336. to: &mut R,
  337. ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
  338. #[cfg(all(debug_assertions, feature = "hot-reload"))]
  339. {
  340. // In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
  341. let disconnect_js = dioxus_hot_reload::RECONNECT_SCRIPT;
  342. to.write_str(r#"<script>"#)?;
  343. to.write_str(disconnect_js)?;
  344. to.write_str(r#"</script>"#)?;
  345. }
  346. let ServeConfig { index, .. } = &self.cfg;
  347. to.write_str(&index.post_main)?;
  348. Ok(())
  349. }
  350. /// Render all content after the body of the page.
  351. pub fn render_after_body<R: std::fmt::Write>(
  352. &self,
  353. to: &mut R,
  354. ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
  355. let ServeConfig { index, .. } = &self.cfg;
  356. to.write_str(&index.after_closing_body_tag)?;
  357. Ok(())
  358. }
  359. }
  360. fn pre_renderer() -> Renderer {
  361. let mut renderer = Renderer::default();
  362. renderer.pre_render = true;
  363. renderer
  364. }