AJAX重复请求响应渐缓卡顿问题求助及代码优化建议
搜索框重复输入时响应卡顿优化求助
当在搜索框中反复输入字符时,系统响应逐渐变慢直至卡死。目前数据获取正常,但不刷新页面重复搜索时会出现卡顿。以下是当前实现代码,求优化建议:
模型代码(C#)
public class TrieNode { public List<string> Titles { get; set; } public Dictionary<char, TrieNode> Children { get; set; } public bool IsEndOfWord { get; set; } public int Popularity { get; set; } public TrieNode() { Titles = new List<string>(); Children = new Dictionary<char, TrieNode>(); } }
服务代码(C#)
public void Insert(string title) { var node = _trie; foreach (var ch in title) { if (!node.Children.ContainsKey(ch)) { node.Children[ch] = new TrieNode(); } node = node.Children[ch]; } if (node.Titles != null) { node.Titles.Add(title); } else { node.IsEndOfWord = true; } if (node.Titles != null && node.Titles.Count >= Threshold) { ConvertListToTrie(node); } } private void ConvertListToTrie(TrieNode node) { foreach (var title in node.Titles) { Insert(title); } node.Titles = null; } /* Search to get exact titles and with levenshtein then sort by latest visit first then popularity and titles asc then lastly with levenshtein*/ public List<(string title, int popularity)> Search(string prefix) { var node = _trie; foreach (var ch in prefix) { if (!node.Children.ContainsKey(ch)) { return new List<(string, int)>(); } node = node.Children[ch]; } return GettitlesFromNode(prefix, node); } private List<(string title, int popularity)> GettitlesFromNode(string prefix, TrieNode node) { var titles = new List<(string title, int popularity)>(); if (node.IsEndOfWord) { titles.Add((prefix, node.Popularity)); } foreach (var child in node.Children.OrderByDescending(c => c.Value.Popularity)) { titles.AddRange(GettitlesFromNode(prefix + child.Key, child.Value)); } return titles; } public List<(string title, int popularity)> GetSuggestionsWithLevenshtein(string query) { var exactMatches = Search(query); var additionalSuggestions = new List<(string title, int popularity)>(); if (exactMatches.Count < 10) { additionalSuggestions = FindSimilarTitles(query) .Select(w => (title: w, popularity: GetPopularityFromTrie(w))) .Where(w => !exactMatches.Any(s => s.title == w.title)) .Take(10 - exactMatches.Count) .ToList(); } return exactMatches.Select(s => (s.title, s.popularity)) .Concat(additionalSuggestions) .OrderByDescending(s => s.popularity) .ThenBy(s => s.title) .ThenBy(s => Levenshtein(query, s.title)) .ToList(); } /* Get Popularity and Last Visit of each title */ private int GetPopularityFromTrie(string title) { var node = _trie; foreach (var ch in title) { if (!node.Children.ContainsKey(ch)) { return 0; } node = node.Children[ch]; } return node.IsEndOfWord ? node.Popularity : 0; } /* Find Titles with levenshtein distance */ private List<string> FindSimilarTitles(string input) { var results = new List<string>(); foreach (var title in GetAllTitles(_trie, "")) { if (Levenshtein(input, title) <= 2) { results.Add(title); } } return results; } public List<string> GetAllTitles(TrieNode node, string prefix) { var titles = new List<string>(); if (node.IsEndOfWord) { titles.Add(prefix); } foreach (var child in node.Children) { titles.AddRange(GetAllTitles(child.Value, prefix + child.Key)); } return titles; } private int Levenshtein(string input, string target) { if (input == target) { return 0; } if (input.Length == 0) { return target.Length; } if (target.Length == 0) { return input.Length; } int[,] distance = new int[input.Length + 1, target.Length + 1]; for (int i = 0; i <= input.Length; i++) { distance[i, 0] = i; } for (int j = 0; j <= target.Length; j++) { distance[0, j] = j; } for (int i = 1; i <= input.Length; i++) { for (int j = 1; j <= target.Length; j++) { int cost = input[i - 1] == target[j - 1] ? 0 : 1; distance[i, j] = Math.Min(Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1), distance[i - 1, j - 1] + cost); } } return distance[input.Length, target.Length]; }
控制器代码(C#)
static TrieController() { var titles = System.IO.File.ReadAllLines("./assets/titles.txt"); foreach (var title in titles) { if (Regex.IsMatch(title, "^[a-z\\s]+$", RegexOptions.IgnoreCase)) { _trieService.Insert(title.ToLower()); } } } [HttpGet] public IActionResult GetSuggestions(string title) { var suggestions = _trieService.GetSuggestionsWithLevenshtein(title) .Select(s => new { title = s.title, popularity = s.popularity }); return Ok(suggestions.Take(10)); }
前端app.js代码
let visitedTitles = JSON.parse(localStorage.getItem('visitedTitles')) || []; /* Suggestions */ function getSuggestions() { const query = $('#search-box').val(); if (query.length === 0) { $('#suggestions').empty(); return; } $.ajax({ //url: `http://ec2-3-25-135-98.ap-southeast-2.compute.amazonaws.com/trie/`, url: `https://localhost:7223/trie/`, data: { title: query }, success: function (data) { displaySuggestions(data); } }); $('#clear-button').click(function() { $('#search-box').val(''); $('#suggestions').empty(); $(this).hide(); }); $('#search-box').on('input', function() { const query = $(this).val(); if (query.length > 0) { $('#clear-button').show(); } else { $('#clear-button').hide(); } getSuggestions(); }); } function displaySuggestions(suggestions) { const suggestionList = $('#suggestions'); suggestionList.empty(); const sortData = suggestions.map(title => { const visitedItem = visitedTitles.find(item => item.title === title.title); return { ...title, visitDate: visitedItem ? visitedItem.date : null }; }); sortData.sort((a, b) => { const dateA = new Date(a.visitDate); const dateB = new Date(b.visitDate); if (isNaN(dateA.getTime()) && isNaN(dateB.getTime())) { return 0; } else if (isNaN(dateA.getTime())) { return 1; } else if (isNaN(dateB.getTime())) { return -1; } else { return dateB.getTime() - dateA.getTime(); } }); sortData.forEach(suggestion => { const isVisited = visitedTitles.some(item => item.title === suggestion.title); if (isVisited) { const visitedItem = visitedTitles.find(item => item.title === suggestion.title); lastVisited = calculateTimeDifference(visitedItem.date); } const suggestionItems = $( '<li class="list-group-item border-bottom d-flex btn btn-outline-success" width="200px;">' + '<span class="suggestion-title "><i class="fa-solid fa-magnifying-glass fa-xs me-3" style="color: #c4c4c4;"></i>' + suggestion.title + '</span>' + '<span class="text-body-tertiary ms-2" style="scale: 0.7;"><i class="fa-duotone fa-solid fa-eye fa-sm me-1"></i>' + suggestion.popularity + '</span>' + '</li>' ); if(isVisited) { suggestionItems.addClass('visited'); } suggestionList.append(suggestionItems); }); $('#suggestions').on('click', 'li', function () { const title = $(this).find('.suggestion-title').text(); const currentDate = new Date(); const gmtPlus8Date = new Date(currentDate.getTime() + 8 * 60 * 60 * 1000); const formattedDate = gmtPlus8Date.toISOString().slice(0, 19); const visitedItem = { title: title, date: formattedDate }; visitedTitles.push(visitedItem); getByName(title); visitedTitles.push(title); localStorage.setItem('visitedTitles', JSON.stringify(visitedTitles)); $('#search-box').val(''); $('#suggestions').empty(); }); } $(document).ready(function() { const debouncedGetSuggestions = debounce(getSuggestions, 300); $('#search-box').on('input', debouncedGetSuggestions, function() { currentSearchPage = 1; getSuggestions(); }); })
核心优化建议
后端性能瓶颈修复
避免全量遍历计算编辑距离
FindSimilarTitles中调用GetAllTitles会遍历整个Trie树,数据量越大耗时越长。替换为在Trie树中进行有限深度的模糊搜索,只遍历与查询前缀编辑距离≤2的分支,减少计算量。- 预计算并缓存高频词的编辑距离结果,避免重复计算。
优化Trie树的搜索与排序逻辑
GettitlesFromNode中每次递归都对子节点按Popularity排序,递归次数多会累积性能损耗。可以在插入时维护子节点的有序性(比如用SortedDictionary),或者只在需要返回结果时进行一次全局排序。- 限制
Search方法返回的结果数量,比如提前取前20条,避免一次性生成大量数据。
修复Insert方法的逻辑问题
ConvertListToTrie中循环调用Insert会导致重复插入相同标题,造成Trie节点冗余。添加去重判断,或者在插入时直接标记节点的IsEndOfWord并维护Popularity,避免用List暂存后再转Trie的逻辑。
前端性能优化
修复防抖逻辑的重复调用
- 当前代码中
$(document).ready里同时绑定了防抖后的方法和直接调用getSuggestions,导致每次输入触发两次请求。移除重复调用,补充标准防抖函数:function debounce(func, delay) { let timeoutId; return function() { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, arguments), delay); }; } $(document).ready(function() { const debouncedGetSuggestions = debounce(getSuggestions, 300); $('#search-box').on('input', function() { currentSearchPage = 1; debouncedGetSuggestions(); }); })
- 当前代码中
优化DOM操作与数据处理
- 将
visitedTitles转换成Map结构,减少重复查找的时间:const visitedMap = new Map(visitedTitles.map(item => [item.title, item])); - 批量创建DOM元素后一次性插入,避免频繁调用
append:const fragment = document.createDocumentFragment(); sortData.forEach(suggestion => { fragment.appendChild(suggestionItems[0]); }); suggestionList.append(fragment);
- 将
修复visitedTitles的重复添加问题
- 点击建议项时,代码同时添加对象和字符串到数组,导致后续查找逻辑出错。移除
visitedTitles.push(title),只保留对象格式的记录。
- 点击建议项时,代码同时添加对象和字符串到数组,导致后续查找逻辑出错。移除
内容的提问来源于stack exchange,提问作者Mark Diaz
相关产品推荐
相关产品推荐

