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
相关产品推荐
相关产品推荐

