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
PRO

November 11, 2021
Tweet

More Decks by LINE DEVDAY 2021

Other Decks in Technology

Transcript

  1. None
  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. 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
  4. LINE NEWS - MAU: 77 million - MPV: 15.4 billion

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

    - Pages are arranged in tabs - Multiple gateways to articles
  6. 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
  7. Layout Shift

  8. Layout Shift

  9. APIs causing the problem

  10. NEWS Tab APIs Personalization A/B Testing Advertisements

  11. Personalization API Features Affects every user Response time is slow

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

    users Release features gradually to users User A User B
  13. Advertisements API Features Personalized Manage inventory

  14. NEWS Tab APIs Personalization A/B Testing Advertisements

  15. Adaptation of skeleton screens ? ? ?

  16. Skeleton screens behavior

  17. NEWS Tab APIs Personalization A/B Testing Advertisements

  18. Our own skeleton screens

  19. It's just one skeleton screen

  20. It's just one skeleton screen

  21. It's just one skeleton screen

  22. It's just one skeleton screen

  23. - Wait for all APIs to be executed ︙ Conditions

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

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

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

    all APIs to be executed ︙ Not ideal Conditions in which the skeleton screen disappears
  27. Fixed Fixed Fixed Minimum conditions

  28. Minimum conditions

  29. Target Target Target Non-target In viewport Minimum conditions

  30. Target Target Target User don't care if a layout shift

    occurs In viewport Minimum conditions
  31. Conditions in which the skeleton screen disappears ≒ APIs

  32. APIs + Heights of contents Conditions in which the skeleton

    screen disappears =
  33. Front-end problems LINE NEWS faces - The adaptation of skeleton

    screens - The problem: the layout shift
  34. 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
  35. 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
  36. 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
  37. Front-end architecture of NEWS Tab APIs Stores ︙ <Root />

    <Tab /> <Tab /> <Tab /> Actions
  38. APIs Stores Actions ︙ <Root /> <Tab /> <Tab />

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

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

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

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

    <Tab /> <Tab /> <Tab /> Actions
  43. 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
  44. 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
  45. Apply skeleton screen components APIs Stores Actions ︙ <Root />

    <Tab /> <Tab /> <Tab />
  46. APIs Stores Actions ︙ <Root /> <Skeleton> <Tab /> </Skeleton>

    <Skeleton> <Tab /> </Skeleton> <Skeleton> <Tab /> </Skeleton> Apply skeleton screen components
  47. - 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
  48. - 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
  49. 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
  50. <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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. Conditions in which the skeleton screen disappears

  57. (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
  58. Manage the display of skeleton screens APIs Actions ︙ Stores

    <Root /> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap>
  59. The skeleton screen store export interface SkeletonOverlapState { screenHeight: number;

    tabs: SkeletonOverlapTab[]; } Stores
  60. The skeleton screen store export interface SkeletonOverlapState { screenHeight: number;

    tabs: SkeletonOverlapTab[]; }
  61. 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
  62. export interface SkeletonOverlapState { screenHeight: number; tabs: SkeletonOverlapTab[]; } export

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

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

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

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

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

    <Root /> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap> <SkeletonOverlap> <Tab /> </SkeletonOvelap>
  68. 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
  69. 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
  70. 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
  71. Data status Whether the content can be rendered None Pending

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

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

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

    .then((articles) => { dispatcher.dispatch({ type: ACTION_TYPE_GET_ARTICLES, articles, }); }); }
  75. 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, }); }); }
  76. 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, }); }); }
  77. 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
  78. 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, }); }); }
  79. 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
  80. Dependencies on multiple data status Fixed Fixed Fixed Content Store

    Content Store Content Store Data status Data status Data status
  81. Fixed Fixed Content Store Content Store Content Store Data status

    Data status Content Store Data status Fixed Dependencies on multiple data status
  82. Data A Data B Data C Dependencies on multiple data

    status
  83. Data A Data B Data C AND AND = Dependencies

    on multiple data status
  84. Data A Data B Data C AND AND = Success

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

    Cached Failed Ready Dependencies on multiple data status
  86. 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
  87. 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
  88. 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
  89. Content A Content B Content C Not just the total

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

    Not just the total height but the order matters, too
  91. 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
  92. 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
  93. Ready Ready Not Ready Content A Content B Content C

    Not just the total height but the order matters, too
  94. 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
  95. 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
  96. 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
  97. 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
  98. 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); }
  99. Utilities For hooks export function useRenderReady<T extends HTMLElement>({ dataStatus, onRenderReady,

    }: UseRenderReadyParams<T>) { const rootRef = useRef<T>(null); … return rootRef; }
  100. 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>; }
  101. 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 />
  102. 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
  103. 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>; }
  104. 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>; }
  105. 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
  106. 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>; }
  107. 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
  108. Comparisons in skeleton screen methods Our own skeleton screen Ideal

    skeleton screen
  109. Our own skeleton screen Broken Ideal skeleton screen Comparisons in

    skeleton screen methods
  110. Ideal skeleton screen Broken Broken Our own skeleton screen Comparisons

    in skeleton screen methods
  111. Two problems in our own skeleton screen Skeleton screens not

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

    Skeleton screens not disappearing Hard to detect where the problem is occurring
  113. 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
  114. Debug tools

  115. 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
  116. - 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
  117. ① 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 ②
  118. 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
  119. 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/
  120. Future challenges

  121. 0.5s 0.2s 2.5s 2.5s The most delayed content determines the

    time needed
  122. A hybrid between our own skeleton screen and an ideal

    one
  123. 0.5s 0.2s 2.5s 0.5s Remove specific content from the conditions

  124. Thank you