Mastering Masonry Layouts: High‑Performance Dual‑Column Waterfall in H5
This article explores the concept, use‑cases, and step‑by‑step implementation of a high‑performance dual‑column waterfall (masonry) layout for mobile H5, covering Flexbox structure, dynamic data distribution, error‑correction techniques, and a DP‑based optimal arrangement algorithm.
What Is a Masonry (Waterfall) Layout?
Masonry layout, also known as waterfall layout, is a popular multi‑column design where items of varying heights are placed in a staggered fashion, first used by Pinterest. It differs from traditional pagination by presenting an irregular, visually appealing flow.
When to Use Waterfall Layout
Image‑centric content : Large images are scanned quickly, and pagination would interrupt the immersive experience.
Independent items : When each piece of information is relatively independent, a waterfall layout lets users view diverse content simultaneously.
Low user intent : For browsing without a specific target, the layout encourages longer dwell time and serendipitous discovery.
These scenarios improve user retention and engagement.
Implementation Overview
The recommended solution uses Flexbox for layout and infinite scrolling for data loading. The basic HTML structure (for a WeChat mini‑program) is:
<code><view class="container">
<view class="column-container">
<template is="item-card" wx:key="{{item.id}}" wx:for="{{left}}" data="{{...item}}"/>
</view>
<view class="column-container">
<template is="item-card" wx:key="{{item.id}}" wx:for="{{right}}" data="{{...item}}"/>
</view>
</view></code>The corresponding CSS sets the container to a horizontal flex layout and each column to a vertical flex layout:
<code>.container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-top: 12px;
}
.column-container {
flex: 1 1 0;
margin: 4px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}</code>Data is stored in three arrays:
couponList(all items),
left(left column), and
right(right column). Initial page data:
<code>Page({
data: {
couponList: [],
left: [],
right: []
}
});</code>Scenarios and Algorithms
Three height‑variation scenarios are considered:
A1: Fixed item height – simple alternating placement.
A2: Variable height with estimable ratio – assign each new item to the shorter column.
A3: Completely unknown height (e.g., images without size) – treat as A2 after pre‑fetching image dimensions.
For A1, the distribution code is:
<code>let i = 0;
while (i < couponList.length) {
left.push(couponList[i++]);
if (i < couponList.length) {
right.push(couponList[i++]);
}
}</code>For A2, height ratios are computed and items are allocated based on the current height difference:
<code>function computeRatioHeight(data) {
const screenWidth = 375;
const itemHeight = data.height;
return Math.ceil(screenWidth / itemHeight * 100);
}
function formatData(data) {
let diff = 0;
const left = [];
const right = [];
let i = 0;
while (i < data.length) {
if (diff <= 0) {
left.push(data[i]);
diff += computeRatioHeight(data[i]);
} else {
right.push(data[i]);
diff -= computeRatioHeight(data[i]);
}
i++;
}
return { left, right };
}</code>For A3, image dimensions are obtained asynchronously before placement:
<code>function getImgInfo(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
if (img.complete) {
resolve({ width: img.width, height: img.height });
} else {
img.onload = () => resolve({ width: img.width, height: img.height });
}
});
}</code>Advanced Optimizations
In A2, accumulated pixel errors can cause significant height imbalance. A hidden anchor element (height 0 px) is added at the bottom of each column; after each render, its
offsetTopis measured to correct the height difference.
<code><view class="hidden-archer" id="left-archer"/>
<view class="hidden-archer" id="right-archer"/></code>DP (dynamic programming) is introduced to find an optimal distribution that minimizes the height difference, treating the problem as a knapsack where the target capacity is half of the total height. The core DP routine (simplified) is:
<code>resetLayoutByDp(couponList) {
const { left, right, diffValue } = this.data;
const heights = couponList.map(item => item.height / item.width * 160 + 77);
const bagVolume = Math.round(heights.reduce((sum, cur) => sum + cur, diffValue) / 2);
// DP array construction omitted for brevity
const rightIndex = dp[heights.length - 1][bagVolume].indexes;
// Build leftData and rightData based on rightIndex
// Update page data and recalculate diffValue via selector query
}</code>Finally, column ordering can be refined to prioritize high‑priority items, ensuring they appear earlier in the layout.
Demonstration screenshots (omitted here) show the final effect.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.