Skip to content

Commit

Permalink
run pre-hydration render synchronously.
Browse files Browse the repository at this point in the history
Before this, there was a race condition between the hydration sync and the
rendering before it; this is hard to hit unless you are hydrating dynamic
content from the backend, but becomes near constant if real data is being
passed to listWithKey on the backend and the result being hydrated on the
frontend.
  • Loading branch information
jonored committed Jan 24, 2025
1 parent 6a7782a commit 402e85f
Show file tree
Hide file tree
Showing 3 changed files with 17 additions and 4 deletions.
16 changes: 12 additions & 4 deletions reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ module Reflex.Dom.Builder.Immediate
, runHydrationRunnerTWithFailure
, ImmediateDomBuilderT
, runHydrationDomBuilderT
, ShouldRenderInFrame (..)
, getHydrationMode
, addHydrationStep
, addHydrationStepWithSetup
Expand Down Expand Up @@ -242,6 +243,7 @@ data HydrationDomBuilderEnv t m = HydrationDomBuilderEnv
-- ^ In hydration mode? Should be switched to `HydrationMode_Immediate` after hydration is finished
, _hydrationDomBuilderEnv_switchover :: !(Event t ())
, _hydrationDomBuilderEnv_delayed :: {-# UNPACK #-} !(IORef (HydrationRunnerT t m ()))
, _hydrationDomBuilderEnv_renderInFrame :: !(Behavior t ShouldRenderInFrame)
}

-- | A monad for DomBuilder which just gets the results of children and pushes
Expand Down Expand Up @@ -301,7 +303,7 @@ runHydrationRunnerT m = runHydrationRunnerTWithFailure m (pure ())
runHydrationRunnerTWithFailure
:: (MonadRef m, Ref m ~ IORef, Monad m, PerformEvent t m, MonadFix m, MonadReflexCreateTrigger t m, MonadJSM m, MonadJSM (Performable m))
=> HydrationRunnerT t m a -> IO () -> Maybe Node -> Node -> Chan [DSum (EventTriggerRef t) TriggerInvocation] -> m a
runHydrationRunnerTWithFailure (HydrationRunnerT m) onFailure s parent events = flip runDomRenderHookT events $ flip runReaderT parent $ do
runHydrationRunnerTWithFailure (HydrationRunnerT m) onFailure s parent events = (\e f -> runDomRenderHookT f (constant RenderInFrame) e) events $ flip runReaderT parent $ do
(a, s') <- runStateT m (HydrationState s False)
traverse_ removeSubsequentNodes $ _hydrationState_previousNode s'
when (_hydrationState_failed s') $ liftIO $ putStrLn "reflex-dom warning: hydration failed: the DOM was not as expected at switchover time. This may be due to invalid HTML which the browser has altered upon parsing, some external JS altering the DOM, or the page being served from an outdated cache."
Expand Down Expand Up @@ -355,19 +357,25 @@ newtype DomRenderHookT t m a = DomRenderHookT { unDomRenderHookT :: RequesterT t
#endif
)

data ShouldRenderInFrame = Synchronous | RenderInFrame

{-# INLINABLE runDomRenderHookT #-}
runDomRenderHookT
:: (MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadJSM m, MonadJSM (Performable m), MonadRef m, Ref m ~ IORef)
=> DomRenderHookT t m a
-> Behavior t ShouldRenderInFrame
-> Chan [DSum (EventTriggerRef t) TriggerInvocation]
-> m a
runDomRenderHookT (DomRenderHookT a) events = do
runDomRenderHookT (DomRenderHookT a) renderInFrame events = do
flip runTriggerEventT events $ do
rec (result, req) <- runRequesterT a rsp
rsp <- performEventAsync $ ffor req $ \rm f -> liftJSM $ runInAnimationFrame f $
rsp <- performEventAsync $ ffor (attach renderInFrame req) $ \(should, rm) f -> liftJSM $ runInAnimationFrameIf should f $
traverseRequesterData (fmap Identity) rm
return result
where
runInAnimationFrameIf Synchronous = runWithoutAnimationFrame
runInAnimationFrameIf RenderInFrame = runInAnimationFrame
runWithoutAnimationFrame f x = x >>= liftIO . f
runInAnimationFrame f x = void . DOM.inAnimationFrame' $ \_ -> do
v <- synchronously x
void . liftIO $ f v
Expand Down Expand Up @@ -398,7 +406,7 @@ runHydrationDomBuilderT
-> HydrationDomBuilderEnv t m
-> Chan [DSum (EventTriggerRef t) TriggerInvocation]
-> m a
runHydrationDomBuilderT (HydrationDomBuilderT a) env = runDomRenderHookT (runReaderT a env)
runHydrationDomBuilderT (HydrationDomBuilderT a) env = runDomRenderHookT (runReaderT a env) (_hydrationDomBuilderEnv_renderInFrame env)

instance (RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, Monad m) => HasDocument (HydrationDomBuilderT s t m) where
{-# INLINABLE askDocument #-}
Expand Down
4 changes: 4 additions & 0 deletions reflex-dom-core/src/Reflex/Dom/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ runHydrationWidgetWithHeadAndBodyWithFailure onFailure switchoverAction app = wi
hydrateDom n w = do
delayed <- liftIO $ newIORef $ pure ()
unreadyChildren <- liftIO $ newIORef 0
renderInFrame <- hold Synchronous $ RenderInFrame <$ switchover
lift $ do
let builderEnv = HydrationDomBuilderEnv
{ _hydrationDomBuilderEnv_document = globalDoc
Expand All @@ -176,6 +177,7 @@ runHydrationWidgetWithHeadAndBodyWithFailure onFailure switchoverAction app = wi
, _hydrationDomBuilderEnv_hydrationMode = hydrationMode
, _hydrationDomBuilderEnv_switchover = switchover
, _hydrationDomBuilderEnv_delayed = delayed
, _hydrationDomBuilderEnv_renderInFrame = renderInFrame
}
a <- runHydrationDomBuilderT w builderEnv events
forM_ hydrationResult $ \hr -> do
Expand Down Expand Up @@ -270,6 +272,7 @@ runImmediateWidgetWithHeadAndBody app = withJSContextSingletonMono $ \jsSing ->
, _hydrationDomBuilderEnv_hydrationMode = hydrationMode
, _hydrationDomBuilderEnv_switchover = never
, _hydrationDomBuilderEnv_delayed = delayed
, _hydrationDomBuilderEnv_renderInFrame = constant RenderInFrame
}
lift $ runHydrationDomBuilderT w builderEnv events
runWithJSContextSingleton (runPostBuildT (runTriggerEventT (app (go headFragment) (go bodyFragment)) events) postBuild) jsSing
Expand Down Expand Up @@ -308,6 +311,7 @@ attachWidget' rootElement jsSing w = do
, _hydrationDomBuilderEnv_switchover = never
, _hydrationDomBuilderEnv_delayed = delayed
, _hydrationDomBuilderEnv_hydrationMode = hydrationMode
, _hydrationDomBuilderEnv_renderInFrame = constant RenderInFrame
}
a <- runWithJSContextSingleton (runPostBuildT (runHydrationDomBuilderT w builderEnv events) postBuild) jsSing
return ((a, events), postBuildTriggerRef)
Expand Down
1 change: 1 addition & 0 deletions reflex-dom-core/src/Reflex/Dom/Prerender.hs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ instance (Adjustable t m, PrerenderBaseConstraints js t m, ReflexHost t) => Prer
, _hydrationDomBuilderEnv_delayed = delayed
, _hydrationDomBuilderEnv_hydrationMode = immediateMode
, _hydrationDomBuilderEnv_switchover = never
, _hydrationDomBuilderEnv_renderInFrame = constant Synchronous
}
a0 <- lift $ runHydrationDomBuilderT server serverEnv events
(a', trigger) <- newTriggerEvent
Expand Down

0 comments on commit 402e85f

Please sign in to comment.