You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP分类手风琴加载慢优化 实现资源缓存避免重复加载

性能瓶颈定位

你当前的实现加载慢是三个核心问题叠加导致的:

  • 递归查库的N+1问题:Subcategoriefirst 函数每查询一层子分类就会执行一次SQL,14000条分类对应上千次数据库查询,仅数据库IO就会消耗数秒到数十秒
  • 全量DOM渲染:首屏直接把14000条分类对应的DOM节点全部拼接后塞入页面,浏览器解析HTML、计算样式、绑定事件的开销被拉满,性能较差的设备卡数分钟属于正常现象
  • 无缓存机制:每次页面加载都会重复走「查库→拼接HTML→渲染DOM」的全流程,相同资源被反复加载,产生大量无意义性能消耗

分步优化方案

1. 单次查库+内存组装树结构,消灭N+1查询

不要分层递归查库,一次性拉取全部分类数据,在内存中组装父子树形结构,耗时可以从数秒降到毫秒级:

global $conn;
// 一次性拉取全部分类,不要递归分层查询
$query = "SELECT ID, Name, Parent FROM Categorieen";
$allCats = $conn->query($query)->fetchAll(PDO::FETCH_ASSOC);

$catMap = [];
$rootCats = [];
// 第一轮循环初始化分类映射
foreach ($allCats as $cat) {
    $cat['children'] = [];
    $catMap[$cat['ID']] = $cat;
}
// 第二轮循环组装父子关系
foreach ($catMap as $id => $cat) {
    if ($cat['Parent'] == -1) {
        $rootCats[] = &$catMap[$id];
    } else if (isset($catMap[$cat['Parent']])) {
        $catMap[$cat['Parent']]['children'][] = &$catMap[$id];
    }
}

2. 加服务端缓存,避免重复查库

分类数据不属于高频变更数据,直接把组装好的分类树存在内存缓存中,大部分请求不需要再碰数据库:

$cacheKey = 'category_tree_v1';
// 以APCu缓存为例,也可以换成文件缓存、Redis缓存
if (apcu_exists($cacheKey)) {
    $rootCats = apcu_fetch($cacheKey);
} else {
    // 放上面的查库、组装树逻辑
    apcu_store($cacheKey, $rootCats, 3600); // 缓存1小时,分类更新时主动清除缓存即可
}

3. 改懒加载逻辑,首屏只渲染一级分类

不要首屏渲染所有层级分类,首屏仅输出一级主分类结构,用户点击展开对应分类时再加载子分类,首屏DOM节点可以从14000+降到几十条,渲染耗时直接降到毫秒级。
重构Hoofdcategorie函数:

function Hoofdcategorie($rootCats) {
    $html = '<div class="accordion" id="accordionRoot">';
    foreach ($rootCats as $cat) {
        $id = 'text' . $cat['ID'];
        $labelID = $id . '1234567890';
        $flushID = 'flusID' . $id;
        $buttonName = 'loadbutton' . $id;
        $html .= <<<EOD
    <div class="accordion-item buttonColor">
        <h2 class="accordion-header" id='$labelID'>
            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target='#$id' 
            aria-expanded="false" aria-controls='$id' id="$buttonName" style="background-color:#cc99ff;" data-catid="{$cat['ID']}">
                {$cat['Name']}
            </button>
        </h2>
        <div id='$id' class="accordion-collapse collapse" aria-labelledby='$labelID' data-bs-parent="#accordionRoot">
            <div class='accordion-body'>
                <div class='accordion accordion-flush' id='$flushID' data-loaded="0">
                    <!-- 子分类容器默认留空,展开时再加载内容 -->
                </div>
            </div>
        </div>
    </div>
EOD;
    }
    $html .= '</div>';
    return $html;
}

可选优化:如果分类层级最多3-4层,可以直接把组装好的全量分类树转成JSON输出到页面JS变量中,不需要单独写子分类接口。14000条分类的JSON经gzip压缩后仅几百KB,完全不影响首屏加载速度,还能省掉接口请求开销。

4. 加前端本地缓存,后续访问无需重复加载

监听手风琴展开事件,已经加载过的子分类存在本地缓存中,后续访问直接读缓存,不需要重复请求:

const CACHE_VERSION = 'v1'; // 分类更新时修改版本号即可自动拉取新数据
// 读本地缓存
function getCatCache(pid) {
    return localStorage.getItem(`cat_${CACHE_VERSION}_${pid}`);
}
// 写本地缓存
function setCatCache(pid, html) {
    localStorage.setItem(`cat_${CACHE_VERSION}_${pid}`, html);
}

// 监听Bootstrap手风琴展开事件
document.addEventListener('show.bs.collapse', function(e) {
    const subContainer = e.target.querySelector('.accordion-flush');
    // 已经加载过的直接跳过
    if (subContainer.dataset.loaded === '1') return;
    const catId = e.target.querySelector('button').dataset.catid;
    // 优先读本地缓存
    const cacheContent = getCatCache(catId);
    if (cacheContent) {
        subContainer.innerHTML = cacheContent;
        subContainer.dataset.loaded = '1';
        return;
    }
    // 无缓存则请求接口拉取子分类,接口逻辑根据传入pid返回对应子分类HTML即可
    fetch(`/get_sub_cats.php?pid=${catId}`)
        .then(res => res.text())
        .then(html => {
            subContainer.innerHTML = html;
            subContainer.dataset.loaded = '1';
            setCatCache(catId, html);
        })
});

优化效果
  • 数据库查询从上千次递归查询降为1次全量查询,加服务端缓存后99%的请求不需要访问数据库
  • 首屏DOM节点从14000+降至几十条,首屏加载时间从数分钟降至50ms以内
  • 子分类按需加载,已加载内容存在本地,后续访问不需要重复请求,访问速度会随用户操作进一步提升

内容的提问来源于stack exchange,提问作者JustJob

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 20:42:20