Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solving the Layout Shift Problem in LINE NEWS

Solving the Layout Shift Problem in LINE NEWS

LINE DEVDAY 2021

November 11, 2021
Tweet

More Decks by LINE DEVDAY 2021

Other Decks in Technology

Transcript

  1. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Two problems in our own skeleton screen - What changed and future challenges
  2. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Two problems in our own skeleton screen - What changed and future challenges
  3. LINE NEWS - MAU: 77 million - MPV: 15.4 billion

    - A large scale of development - Web front-end development
  4. - Technology - TypeScript - React NEWS Tab - Features

    - Pages are arranged in tabs - Multiple gateways to articles
  5. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Two problems in our own skeleton screen - What changed and future challenges
  6. Personalization API Features Affects every user Response time is slow

    Cannot use CDN cache Articles related to cars
  7. A/B Testing API Features Expose different UIs for sets of

    users Release features gradually to users User A User B
  8. - Wait for all APIs to be executed ︙ Conditions

    in which the skeleton screen disappears
  9. - Wait for all APIs to be executed Not ideal

    ︙ Conditions in which the skeleton screen disappears
  10. Not ideal - Wait for some APIs - Wait for

    all APIs to be executed ︙ Conditions in which the skeleton screen disappears
  11. Not ideal - Wait for some APIs - Wait for

    all APIs to be executed ︙ Not ideal Conditions in which the skeleton screen disappears
  12. Target Target Target User don't care if a layout shift

    occurs In viewport Minimum conditions
  13. Front-end problems LINE NEWS faces - The adaptation of skeleton

    screens - The problem: the layout shift
  14. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Two problems in our own skeleton screen - What changed and future challenges
  15. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two heights required to hide the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  16. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two heights required to hide the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  17. APIs Stores Actions ︙ <Root /> <Tab /> <Tab />

    <Tab /> Front-end architecture of NEWS Tab
  18. APIs Stores Actions ︙ <Root /> <Tab /> <Tab />

    <Tab /> Front-end architecture of NEWS Tab
  19. APIs Actions ︙ Stores <Root /> <Tab /> <Tab />

    <Tab /> Front-end architecture of NEWS Tab
  20. ︙ <Root /> <Tab /> <Tab /> <Tab /> Front-end

    architecture of NEWS Tab Stores Actions APIs
  21. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two heights required to hide the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  22. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two heights required to hide the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  23. APIs Stores Actions ︙ <Root /> <Skeleton> <Tab /> </Skeleton>

    <Skeleton> <Tab /> </Skeleton> <Skeleton> <Tab /> </Skeleton> Apply skeleton screen components
  24. - A component with only style applied - Does not

    contain business logic - Switch the display of skeleton screen component - Manage fade-out animation <SkeletonScreen /> <SkeletonOverlap /> Two components used to create the skeleton screen
  25. - Switch the display of skeleton screen component - Manage

    fade-out animation <SkeletonScreen /> <SkeletonOverlap /> The SkeletonScreen component: 1/2 - A component with only style applied - Does not contain business logic
  26. export const SkeletonScreen = (props: Props) => { return (

    <div className="skeleton” onTransitionEnd={props.onTransitionEnd}> <div className="loading" /> <div className="component1"> ... </div> ... </div> ); }; <SkeletonScreen /> The SkeletonScreen component: 1/2
  27. <SkeletonScreen /> <SkeletonOverlap /> The SkeletonOverlap component: 2/2 - A

    component with only style applied - Does not contain business logic - Switch the display of skeleton screen component - Manage fade-out animation
  28. export const SkeletonOverlap = props => { const [state, done]

    = useAnimationSkeletonOverlap(props.readyToHide); return ( <div className={transformAnimationToStyle(state)}> {state !== ANIMATION_DONE && ( <SkeletonScreen onTransitionEnd={done} /> )} {props.children} </div> ); }; <SkeletonOverlap /> The SkeletonOverlap component: 2/2
  29. export const SkeletonOverlap = props => { const [state, done]

    = useAnimationSkeletonOverlap(props.readyToHide); return ( <div className={transformAnimationToStyle(state)}> {state !== ANIMATION_DONE && ( <SkeletonScreen onTransitionEnd={done} /> )} {props.children} </div> ); }; <SkeletonOverlap /> The SkeletonOverlap component: 2/2
  30. export const SkeletonOverlap = props => { const [state, done]

    = useAnimationSkeletonOverlap(props.readyToHide); return ( <div className={transformAnimationToStyle(state)}> {state !== ANIMATION_DONE && ( <SkeletonScreen onTransitionEnd={done} /> )} {props.children} </div> ); }; <SkeletonOverlap /> The SkeletonOverlap component: 2/2
  31. export const SkeletonOverlap = props => { const [state, done]

    = useAnimationSkeletonOverlap(props.readyToHide); return ( <div className={transformAnimationToStyle(state)}> {state !== ANIMATION_DONE && ( <SkeletonScreen onTransitionEnd={done} /> )} {props.children} </div> ); }; <SkeletonOverlap /> The SkeletonOverlap component: 2/2
  32. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two heights required to hide the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  33. (1) The height of viewport (The skeleton screen) (2) The

    total height of fixed contents If (2) is greater than (1), the skeleton screen disappears Conditions in which the skeleton screen disappears
  34. Manage the display of skeleton screens APIs Actions ︙ Stores

    <Root /> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap>
  35. export interface SkeletonOverlapTab { [index: number]: { elementHeight: number; };

    totalElementHeight: number; readyToHide: boolean; } export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } new ResizeObserver(() => { setScreenSize(document.documentElement.clientHeight); }); The skeleton screen store
  36. export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } export

    interface SkeletonOverlapTab { [index: number]: { elementHeight: number; }; totalElementHeight: number; readyToHide: boolean; } The skeleton screen store
  37. export interface SkeletonOverlapTab { [index: number]: { elementHeight: number; };

    totalElementHeight: number; readyToHide: boolean; } export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } The skeleton screen store
  38. export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } export

    interface SkeletonOverlapTab { [index: number]: { elementHeight: number; }; totalElementHeight: number; readyToHide: boolean; } The skeleton screen store
  39. export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } export

    interface SkeletonOverlapTab { [index: number]: { elementHeight: number; }; totalElementHeight: number; readyToHide: boolean; } The skeleton screen store
  40. export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } export

    interface SkeletonOverlapTab { [index: number]: { elementHeight: number; }; totalElementHeight: number; readyToHide: boolean; } The skeleton screen store
  41. Manage the display of skeleton screens APIs Actions ︙ Stores

    <Root /> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap>
  42. Agenda - Overview of LINE NEWS - The layout shift

    problem and the adaptation of skeleton screens - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two heights required to hide the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  43. Fixed Fixed Fixed Content Store Content Store Content Store API

    API API Data status Data status Data status How and when the content height is determined
  44. Data status Success Pending None The data doesn't exist yet

    Data is available Not ready to render Not ready to render Ready to render In the process of retrieving data
  45. Data status Whether the content can be rendered None Pending

    Success 㾎 Failed 㾎 Cached 㾎 Cached & Pending 㾎
  46. Data status Stores Actions (Dispatcher) APIs getArticles() { return API.fetchArticles(),

    .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES, articles, }); }); }
  47. Data status Stores Actions (Dispatcher) APIs getArticles() { return API.fetchArticles(),

    .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES, articles, }); }); }
  48. APIs Data status Stores Actions (Dispatcher) getArticles() { return API.fetchArticles(),

    .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES, articles, }); }); }
  49. Data status getArticles() { return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({

    type: ACTION_TYPE_GET_ARTICLES, articles, }); }); } getArticles() { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLE_PENDING, }); return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES_SUCCESS, articles, }); }) .catch(() => { dispatcher.dispatch ({ type: ACTION_TYPE_GET_ARTICLES_FAILED, }); }); }
  50. Data status None Data status flows getArticles() { return API.fetchArticles(),

    .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES, articles, }); }); } getArticles() { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLE_PENDING, }); return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES_SUCCESS, articles, }); }) .catch(() => { dispatcher.dispatch ({ type: ACTION_TYPE_GET_ARTICLES_FAILED, }); }); }
  51. getArticles() { return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES,

    articles, }); }); } getArticles() { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLE_PENDING, }); return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES_SUCCESS, articles, }); }) .catch(() => { dispatcher.dispatch ({ type: ACTION_TYPE_GET_ARTICLES_FAILED, }); }); } Data status None Pending Data status flows
  52. Data status None Data status flows Success Pending getArticles() {

    return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES, articles, }); }); } getArticles() { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLE_PENDING, }); return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES_SUCCESS, articles, }); }) .catch(() => { dispatcher.dispatch ({ type: ACTION_TYPE_GET_ARTICLES_FAILED, }); }); }
  53. Data status getArticles() { return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({

    type: ACTION_TYPE_GET_ARTICLES, articles, }); }); } getArticles() { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLE_PENDING, }); return API.fetchArticles(), .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES_SUCCESS, articles, }); }) .catch(() => { dispatcher.dispatch ({ type: ACTION_TYPE_GET_ARTICLES_FAILED, }); }); } None Success Failed Data status flows Pending
  54. Dependencies on multiple data status Fixed Fixed Fixed Content Store

    Content Store Content Store Data status Data status Data status
  55. Fixed Fixed Content Store Content Store Content Store Data status

    Data status Content Store Data status Fixed Dependencies on multiple data status
  56. Data A Data B Data C AND AND = Success

    Cached Pending Not Ready Dependencies on multiple data status
  57. Data A Data B Data C AND AND = Success

    Cached Failed Ready Dependencies on multiple data status
  58. Fixed Fixed Fixed Content Store Content Store Content Store Data

    status Data status Content Store Data status Store parameters to determine when each skeleton screen disappears
  59. Content Store Content Store Content Store Data status Data status

    Content Store SkeletonScreen Store Data status Height Height Height Store parameters to determine when each skeleton screen disappears
  60. SkeletonScreen Store Height, Index Height, Index Height, Index Content Store

    Content Store Content Store Data status Data status Content Store Data status Store parameters to determine when each skeleton screen disappears
  61. Content A Content B Content C Not just the total

    height but the order matters, too
  62. Ready Ready Not Ready Content A Content B Content C

    Not just the total height but the order matters, too
  63. Content A Content C + > = Height of viewport

    Should we hide the skeleton in this case? Not just the total height but the order matters, too
  64. Ready Ready Not Ready Ready Ready Ready Content A Content

    B Content C Content A Content B Content C Not just the total height but the order matters, too
  65. Ready Ready Not Ready Content A Content B Content C

    Not just the total height but the order matters, too
  66. Ready Ready Not Ready Content A Content B Content C

    Content A Content B Content C Ready Ready Not Ready Not just the total height but the order matters, too
  67. Ready Ready Not Ready Content A Content B Content C

    Content A Content B Content C Ready Ready Not Ready Not just the total height but the order matters, too
  68. SkeletonScreen Store Height, Index Height, Index Height, Index Content Store

    Content Store Content Store Data status Data status Content Store Data status Store parameters to determine when each skeleton screen disappears
  69. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Front-end architecture of NEWS Tab - Skeleton screen components - Two height to disappear the skeleton screen - How and when the content height is determined - Utilities - Two problems in our own skeleton screen - What changed and future challenges
  70. Utilities For hooks export function useRenderReady<T extends HTMLElement>({ dataStatus, onRenderReady,

    }) { … } For class components export function RenderReadyWrapper<T extends HTMLElement = HTMLElement>({ dataStatus, onRenderReady, childrenCallback, }: RenderReadyWrapperProps<T>) { const rootRef = useRenderReady<T>({ … }); return childrenCallback(rootRef); }
  71. Utilities For hooks export function useRenderReady<T extends HTMLElement>({ dataStatus, onRenderReady,

    }: UseRenderReadyParams<T>) { const rootRef = useRef<T>(null); … return rootRef; }
  72. Utilities Usage const Content = (props) => { const ref

    = useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; }
  73. Utilities Usage const Content = (props) => { const ref

    = useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; } useRenderReady <div />
  74. Utilities Usage const Content = (props) => { const ref

    = useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; } useRenderReady Data status ︙ Data status
  75. Utilities Usage const Content = (props) => { const ref

    = useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; }
  76. Utilities Usage const Content = (props) => { const ref

    = useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; }
  77. Usage const Content = (props) => { const ref =

    useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; } Utilities
  78. Utilities Usage const Content = (props) => { const ref

    = useRenderReady({ onRenderReady: (elementHeight) => { readyContent( props.tabIndex, props.contentIndex, props.elementHeight ); }, dataStatus: [props.dataStatus1, props.dataStatus2], }); return <div ref={ref} >… </div>; }
  79. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Two problems in our own skeleton screen - What changed and future challenges
  80. Two problems in our own skeleton screen Skeleton screens not

    disappearing Hard to detect where the problem is occurring
  81. Set a timeout for displaying the skeleton screen The solutions

    Skeleton screens not disappearing Hard to detect where the problem is occurring
  82. Provide a dedicated debugging mechanism Set a timeout for displaying

    the skeleton screen Skeleton screens not disappearing Hard to detect where the problem is occurring The solutions
  83. A debugger for the skeleton screen - Enable or disabled

    - Select a tab - The height of viewport - The height of contents - Ready or not status for contents - The index of contents - Targeted contents for the skeleton screen disappearing - Number of consecutive contents
  84. - Enable or disabled - Select a tab - The

    height of viewport - The height of contents - Ready or not status for contents… ① - The index of contents - Targeted contents for the skeleton screen disappearing… ② - Number of consecutive contents A debugger for the skeleton screen ① Ready or not
  85. ① Ready or not - Enable or disabled - Select

    a tab - The height of viewport - The height of contents - Ready or not status for contents… ① - The index of contents - Targeted contents for the skeleton screen disappearing… ② - Number of consecutive contents A debugger for the skeleton screen Highlighted in red ②
  86. Agenda - Overview of LINE NEWS - Front-end problems LINE

    NEWS faces - Implementation of the skeleton screen - Two problems in our own skeleton screen - What changed and future challenges
  87. Cumulative Layout Shift (CLS) 0.348 0.002 “To provide a good

    user experience, sites should strive to have a CLS score of 0.1 or less.” https://web.dev/i18n/en/cls/