最近接到了一个新需求,要在PHP里渲染一个超大的数据集,数据量大概在百万级别。作为一个老PHP,我一开始以为这不就是个简单的foreach循环,结果现实给我上了一课。今天就来聊聊PHP大数据渲染的那些坑,以及我是怎么填这些坑的。
先说说最初的想法:直接从数据库里拉出来所有数据,然后扔给PHP处理,循环输出到页面上。代码大概长这样:
$data = $db->query("SELECT FROM huge_table");
foreach ($data as $row) {
echo "<div>{ $row['name'] }</div>";
}
看起来很简单?结果服务器直接崩了。为啥?内存爆炸了。PHP脚本默认的内存限制是128M,但百万条记录一上来就是几百兆,直接爆了。
所以,填坑第一步:控制内存。我决定分批处理数据,比如每次只处理1000条。于是代码改成了这样:
$offset = 0;
$limit = 1000;
do {
$data = $db->query("SELECT
FROM huge_table LIMIT { $limit } OFFSET { $offset }");foreach ($data as $row) {
echo "<div>{ $row['name'] }</div>";
}
$offset += $limit;
} while (!empty($data));
这下内存是稳住了,但新问题又来了:渲染时间太长。百万条记录,每批1000条,要渲染1000次,每次都要跟数据库交互,速度实在是太慢了。于是,填坑第二步:减少数据库查询次数。
我决定用游标(cursor)来处理数据,一次性从数据库拉取数据,然后逐条处理。代码是这样的:
$stmt = $db->query("SELECT FROM huge_table");
while ($row = $stmt->fetch()) {
}
这样减少了数据库查询次数,速度确实快了不少。但问题又来了:PHP脚本执行时间太长,超时了。PHP默认的最大执行时间是30秒,而我这个脚本跑了1分钟还没完。于是,填坑第三步:调整脚本执行时间。
我在脚本开头加了一句:
set_time_limit(0);
这样脚本就不会超时了。但新的问题又出现了:页面加载时间太长,用户会以为网页挂了。于是,填坑第四步:异步渲染。

我决定用AJAX来分批渲染数据,先把页面框架加载出来,然后通过AJAX请求分批获取数据并渲染。前端代码大概长这样:
function loadMore(offset, limit) {
fetch(/api/getData?offset=${ offset }&limit=${ limit })
.then(response => response.json())
.then(data => {
data.forEach(row => {
document.getElementById('container').innerHTML += <div>${ row.name }</div>;
});
if (data.length > 0) {
loadMore(offset + limit, limit);
});
}
loadMore(0, 1000);
后端API代码大概是这样的:
$offset = $_GET['offset'];
$limit = $_GET['limit'];
$data = $db->query("SELECT
FROM huge_table LIMIT { $limit } OFFSET { $offset }");echo json_encode($data);
这下用户体验好多了,页面先加载出来,数据慢慢渲染。但问题又来了:如果用户快速滚动页面,AJAX请求会堆积,导致服务器压力过大。于是,填坑第五步:限制并发请求。
我决定在前端加一个队列,每次只允许一个AJAX请求,等上一个请求完成了再发起下一个。代码大概是这样的:
let isRequesting = false;
if (isRequesting) return;
isRequesting = true;
isRequesting = false;
}
这下服务器压力小多了,但新问题又来了:如果用户不滚动页面,数据就不会加载,导致页面一开始是空的。于是,填坑第六步:预加载数据。

我决定在页面加载时先加载几批数据,这样用户一进来就能看到内容。loadMore(0, 5000); // 先加载5000条
这样用户体验更好了,但问题又来了:如果用户只看前几页,后面的数据就白白加载了,浪费资源。于是,填坑第七步:仅渲染可视区域。
我决定用虚拟滚动技术,只渲染用户当前可见的部分,其他部分等用户滚动到那里时再渲染。前端代码大概是这样的:
const container = document.getElementById('container');
const visibleItems = 20;
const itemHeight = 50;
let scrollTop = 0;
function render() {
const start = Math.floor(scrollTop / itemHeight);
const end = start + visibleItems;
fetch(/api/getData?offset=${ start }&limit=${ visibleItems })
container.innerHTML = '';
data.forEach((row, index) => {
const top = (start + index) itemHeight;
container.innerHTML += <div style="position: absolute; top: ${ top }px;">${ row.name }</div>;
}
container.addEventListener('scroll', () => {
scrollTop = container.scrollTop;
render();
});
render();
这下资源利用率大大提高了,但问题又来了:快速滚动时,渲染跟不上,导致页面卡顿。于是,填坑第八步:优化渲染性能。
我决定用节流(throttle)来控制渲染频率,比如每100ms最多渲染一次。let lastRenderTime = 0;
const throttleTime = 100;
const now = Date.now();
if (now - lastRenderTime > throttleTime) {
lastRenderTime = now;
scrollTop = container.scrollTop;
render();
}
});
这下页面流畅多了,但新问题又来了:如果用户快速滚动到页面底部,需要渲染的数据量过大,导致渲染时间过长。于是,填坑第九步:分块渲染。
我决定把数据分成多个块,每个块单独渲染,减少单次渲染的数据量。const chunkSize = 1000;
let currentChunk = 0;
function renderChunk() {
const start = currentChunk
chunkSize;const end = start + chunkSize;
fetch(/api/getData?offset=${ start }&limit=${ chunkSize })
currentChunk++;
requestAnimationFrame(renderChunk);
}
if (container.scrollTop + container.clientHeight >= container.scrollHeight) {
renderChunk();
}
});
这样即使是百万级别的数据,也能流畅地渲染出来。不过,问题又来了:如果用户频繁滚动,可能会导致重复渲染。于是,填坑第十步:缓存已渲染的数据。
我决定用个数组来记录哪些数据已经渲染过了,避免重复渲染。const rendered = [];
if (rendered.includes(currentChunk)) return;
rendered.push(currentChunk);
}
这下性能终于达到了预期,用户可以流畅地浏览百万级别的数据了。
总结一下,PHP大数据渲染的坑确实不少,但通过分批处理、异步渲染、虚拟滚动、分块渲染等技术手段,最终还是填上了这些坑。希望这篇分享能帮到同样遇到这类问题的朋友。
1279

被折叠的 条评论
为什么被折叠?



