background

Waterfall flow is a common development scenario, and we have some solutions in our in-house component library. However, these schemes are all suitable for a single scenario, and each implementation scheme has more or less some problems. Based on this, we design and develop a waterfall flow component that is compatible with multiple scenarios.

At present, there are three layout methods used to show commodity flow: card flow, fixed waterfall flow, staggered waterfall flow.

The card stream is presented as a drop – down list. This layout allows the user to focus on a single list item, making it easier to read. It is mainly applied to the entry of the secondary list page of the rotation, with the following effect

The fixed waterfall image area size and height remain unchanged. The uniform height makes the whole interface look neat and visually uncluttered. Mainly applied to some channel page scenarios, the effect is as follows

Staggered waterfall flow is visually represented as an uneven multi-column layout composed of elements of equal width and variable height, which is chosen by the home page and the detailed recommendation page for carrying

Problems with existing programs

Of the above three scenarios, the first and second scenarios have fixed image heights and are relatively simple to implement. You can directly use the infinite loading List component. It is the third scenario that often causes problems: staggered waterfall flows. In this scenario, the height of the image should be obtained after the image is loaded, and then added to the lowest column of the waterfall flow. Otherwise, the calculation of the lowest column will be affected, resulting in columns with different lengths.

In zhuan company, the realization of staggered waterfall flow mainly has the following schemes

  • Scheme 1: the left and right column layout is adopted, and the waterfall flow data of the first page is evenly divided and rendered. When data is rendered on the second page, the first data on the second page will be taken out and rendered to the lowest column, and IntersectionObserve will be monitored. After this element appears in the window, the second data will be taken out from the data source and added to the new lowest rendering column, so as to realize the waterfall flow with lazy loading in a loop and back

    • Advantages: AdoptionIntersectionObserveWaterfall flow lazy loading, simple logic
    • Disadvantages:
      1. The column layout supports only two columns, not multiple columns.
      2. The data on the first page do not conform to the specification of waterfall flow, and there is a probability that one column is long and one column is short.
      3. IntersectionObserveCompatibility problems;
      4. No data loading event is exposed, which makes it easy to pull down the request twice interface when working with infinite loading components
  • Scheme 2: Adopt width percentage for style layout, and enable IntersectionObserve monitoring on the first screen rendering. After the element appears in the window, set a setTimeout to load the next waterfall flow element, and add an attribute identifier on the DOM to prevent secondary triggering.

    • Advantages: support multi-column layout of parameter configuration, the first screen conforms to the specification of waterfall stream, and exposes the event after the waterfall stream is loaded. With infinite loading, there will be no two interface requests
    • Disadvantages:IntersectionObserveCompatibility issues remain unresolved; High frequency of internal DOM query and operation; Coupling the logic of infinite loading List, high maintenance cost;setTimeoutThere is no guarantee that the image will load in the correct time sequence, resulting in inaccurate retrieval of the lowest column
  • Scheme 3: Use absolute positioning layout scheme. The implementation principle is to create an image object inside each sub-component waterall-item, listen for the onload event and then trigger the parent component Waterfall to rearrange the waterfall stream.

    • Advantages: Simple internal logic, easy to maintain, but also complies with waterfall flow specifications, provides several commonly used waterfall flow configuration items, after complete loading will trigger events to notify external components
    • Disadvantages: does not support lazy loading of images; Too many redraws (1+2+… +N), not very performance friendly; The time to trigger the redraw is not the most precise time node (via the onload event after the new image, instead of binding the onload event to the current image)

Then I went online to find open source solutions. Here are the top 4 start solutions on Github

  • vue-waterfall: indicates the scheme with the largest number of starts
    • Disadvantages: You need to know the width and height of the image before the component renders, and we generally don’t return these data in the interface
  • vue-waterfall-easy: There is no need to obtain the width and height information of the image in advance, and the image is preloaded before typesetting.
    • Disadvantages: Coupling pull-down, infinite loading components; Include PC side logic, package size is large, for the pursuit of performance page is not friendly (as an open source solution, compatible with more scenarios is not wrong, but we have a separate component implementation of these functions); Load all images at once. Lazy loading is not supported
  • vue-waterfall2: Supports high adaptability and lazy loading
    • Disadvantages: Image objects are created internally multiple times, with a lot of computation and scrolllistening.
  • vue2-waterfall: It can be realized by the encapsulation of the two open source schemes of cropage-layout and ImagesLoaded, with simple and clear logic.
    • Disadvantages: Lazy loading is not supported

Let me summarize it briefly with a picture

New waterfall flow scheme design

Currently, there is no simple, easy-to-use waterfall component on the mobile side, so the plan is to integrate known solutions and implement a new waterfall component. The new waterfall stream will include some of the following benefits:

  • simpleCSSlayout
  • Simplify the logical level of implementation
  • Supports a high degree of adaptation
  • Lazy loading support

The layout scheme

Understand that the waterfall flow CSS layout scheme is mainly divided into three kinds

  • Absolute positioning: solution 3 above and open source solutionvue-waterfall-easyThis layout is suitable for PC waterfall flow
  • Width percentage: above option 2 and open source optionvue-waterfall2Use this layout, but this scheme will have some accuracy problems
  • Flex layout: Some large e-commerce sites like Mogujie use this layout

Among them, Flex layout compatibility and adaptation is not a problem, should be the best solution for mobile layout. So the new waterfall flow will adopt this layout

Waterfall flow logic implementation

There are three logical implementations of waterfall flow

  • newimageObject,onloadGet the original width and height of the image, then calculate the actual rendered height according to the width allocated by the waterfall flow, and mount it as an inline style toDOM
  • Directly splice the width and height information of the picture in the image URL returned by the interface, and advance the layout. Mushroom Street adopts this scheme
  • IntersectionObserverListen for the picture element, which appears in the view and starts fetching data from the column head of the waterfall stream data queue and rendering it to the lowest column of the current waterfall stream, and so on for lazy loading of the waterfall stream

Of the three options, the first is more conventional, and is the way most open source solutions are implemented. But the internal need for height conversion, but also does not support lazy loading pictures.

The second scheme should be a better one, pictures can start typesetting before loading, convenient and simple, but also support lazy loading, user experience is good. Mogujie, Tmall, JINGdong and so on are using this scheme. However, this scenario requires some modifications, such as stitching the image information to the URL before the image is stored, or the back-end interface reads the image object and then returns the image information to the front end. Either the transformation cost is large, or the server pressure will increase, which is not suitable for our business.

The third scheme can support lazy loading without the need for other modifications and should be the most suitable one at present. Therefore, IntersectionObserver will be adopted in the new waterfall stream component to realize the typesetting of the waterfall stream

New waterfall flow concrete implementation

IntersectionObserver compatibility

One of the first problems is thatIntersectionObserverCompatibility issues.IntersectionObserverWhile addressing the performance issues associated with traditional scrolllistening, compatibility has not received a mainstream support, as you can seeiOSThe support is not perfect

The official provided a Polyfill to solve the above problems, but this polyfill is large in volume, which is not friendly to some pages that pursue the ultimate performance, so we adopted the method of dynamic introduction of polyfill

// Dynamically introduce Polyfill in the scenario where IntersectionObserver is not supported
const ioPromise = checkIntersectionObserver()
  ? Promise.resolve()
  : import('intersection-observer')

ioPromise.then(() = > {
  // do something
})
Copy the code

Only in an unsupported IntersectionObserver environment can the Polyfill be loaded, and the testing method is copied from Vue LazyLoad

const inBrowser = typeof window! = ='undefined' && window! = =null

function checkIntersectionObserver() {
  if (
    inBrowser &&
    'IntersectionObserver' in window &&
    'IntersectionObserverEntry' in window &&
    'intersectionRatio' in window.IntersectionObserverEntry.prototype
  ) {
    // Minimal polyfill for Edge 15's lack of `isIntersecting`
    // See: https://github.com/w3c/IntersectionObserver/issues/211
    if(! ('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
      Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
        get: function() {
          return this.intersectionRatio > 0}})}return true
  }
  return false
}
Copy the code

Waterfall stream image loading sequence

Image loading is an asynchronous process, how to ensure the loading sequence of waterfall images?

After the callback function of IntersectionObserver is triggered, the loading of the next waterfall flow image is likely to have different columns of different length and page jitter, because the image may only be partially loaded when the callback is triggered. This problem exists in both option 1 and Option 2 above

View your document, you can see IntersectionObserver provide IntersectionObserverEntry object callback function will provide the following properties

  • Time: indicates the time when visibility changes. It is a high-precision timestamp in milliseconds
  • Target: the target element to be observedDOMThe node object
  • RootBounds: Information about the rectangular region of the root element,getBoundingClientRect()Method, or null if there is no root element (that is, scrolling directly relative to the viewport)
  • BoundingClientRect: Information about the rectangular region of the target element
  • IntersectionRect: information on the intersection area between target element and viewport (or root element)
  • IntersectionRatio: intersectionRatio of target elements, i.eintersectionRectboundingClientRectIs 1 when completely visible and less than or equal to 0 when completely invisible

We can bind the onload event to target and perform the next waterfall data render after onload to ensure the accuracy of the next render when obtaining the lowest column

// Waterfall layout: Take the data at the head of the queue and add it to the column with the smallest waterfall height for rendering. Repeat the loop after the image is fully loaded
observerObj = new IntersectionObserver(
  (entries) = > {
    for (const entry of entries) {
      const { target, isIntersecting } = entry
      if (isIntersecting) {
        if (target.complete) {
          done()
        } else {
          target.onload = target.onerror = done
        }
      }
    }
  }
)
Copy the code

IntersectionObserver triggers twice

It is known that IntersectionObserver is used to monitor target elements and the callback function will be triggered twice when the visibility of target elements changes. Once the target element has just entered the viewport (becoming visible), and once it has completely left the viewport (becoming invisible). To avoid triggering the listening logic a second time, you can stop watching the first time

if (isIntersecting) {
  const done = () = > {
    // Stop the observation to prevent the listening logic from being triggered twice during the pullback
    observerObj.unobserve(target)
  }

  if (target.complete) {
    done()
  } else {
    target.onload = target.onerror = done
  }
}
Copy the code

White screen during first rendering

Due to the serial loading of images, images are rendered one by one, which is very serious when the network is not good, as shown in the following figure

Two solutions are currently available

  • Method 1: Parallel rendering is adopted for the pictures in the first screen rendering, and serial rendering is adopted for the subsequent ones. If the interface returns a page of 20 waterfall elements, the first 1-4 images will be rendered in parallel and the next 5-20 will be rendered in serial. You can adjust firstPageCount to suit your needs, and typically the first screen will render 4-6 images.
waterfall() {
  // Update the minimum waterfall height column
  this.updateMinCol()
  // Take the first column from the data source and add it to the column with the smallest waterfall flow height
  this.appendColData()
  // The first screen uses parallel rendering, and the non-first screen uses serial rendering
  if (++count < this.firstPageCount) {
    this.$nextTick(() = > this.waterfall())
  } else {
    this.$nextTick(() = > this.startObserver())
  }
}
Copy the code

  • Method 2: Add animation to eliminate the impact of the white screen from the visual sense. The component has two built-in animations, which can be transmitted by animation

Blank screen problem during lazy loading

We took the lazy loading approach: loading the next waterfall image after the image is in view is performance-friendly. However, in this case, when the user is scrolling, if the next image is too slow to load, there may be a short blank screen time. How to solve this experience problem

IntersectionObserver has a rootMargin attribute, which can be used to expand the intersecting area so that subsequent data can be loaded in advance. This prevents a white screen when the user scrolls to the bottom and prevents rendering from impacting performance too much. The default is 400px, which is about half of the screen rendered in advance.

// By expanding intersectionRect area, part of data can be loaded in advance to optimize user browsing experience
rootMargin: {
  type: String.default: '0px 0px 400px 0px'
}
Copy the code

How to work with infinite loading components

Generally, for the convenience of maintenance, we separate the infinite loading and waterfall flow logic, so when the waterfall flow data rendering needs to notify external components, otherwise it is easy to trigger the infinite loading logic before the waterfall flow rendering is finished, and send two interface requests.

A judgment can be added to the waterfall stream rendering process to notify the external infinite load component of the next request if there is no data in the queue

const done = () = > {
  if (this.innerData.length) {
    this.waterfall()
  } else {
    this.$emit('rendered')}}Copy the code

Summary & source code

These are some of the problems and solutions that we encountered when making the new waterfall flow component. Of course, this scheme still needs to be optimized and is currently in use as a component block within the company. Code has not been open source, need source partners can pay attention to the public number around FE, reply waterfall flow can get the source code. For waterfall flow components, if you have better opinions and suggestions, welcome to communicate and discuss.