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

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();
  });
})

核心优化建议

后端性能瓶颈修复

  1. 避免全量遍历计算编辑距离

    • FindSimilarTitles中调用GetAllTitles会遍历整个Trie树,数据量越大耗时越长。替换为在Trie树中进行有限深度的模糊搜索,只遍历与查询前缀编辑距离≤2的分支,减少计算量。
    • 预计算并缓存高频词的编辑距离结果,避免重复计算。
  2. 优化Trie树的搜索与排序逻辑

    • GettitlesFromNode中每次递归都对子节点按Popularity排序,递归次数多会累积性能损耗。可以在插入时维护子节点的有序性(比如用SortedDictionary),或者只在需要返回结果时进行一次全局排序。
    • 限制Search方法返回的结果数量,比如提前取前20条,避免一次性生成大量数据。
  3. 修复Insert方法的逻辑问题

    • ConvertListToTrie中循环调用Insert会导致重复插入相同标题,造成Trie节点冗余。添加去重判断,或者在插入时直接标记节点的IsEndOfWord并维护Popularity,避免用List暂存后再转Trie的逻辑。

前端性能优化

  1. 修复防抖逻辑的重复调用

    • 当前代码中$(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();
        });
      })
      
  2. 优化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);
      
  3. 修复visitedTitles的重复添加问题

    • 点击建议项时,代码同时添加对象和字符串到数组,导致后续查找逻辑出错。移除visitedTitles.push(title),只保留对象格式的记录。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 20:29:51