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

ASP.Net页面高效追踪用户活动的实现与优化问题

ASP.NET样板应用中用户活动追踪的优化与实现方案

一、当前代码的性能优化方案

原代码的性能问题

  1. setInterval高频无效执行:每秒固定触发回调,即便用户处于活跃状态,定时器仍持续运行,浪费浏览器资源。
  2. mousemove事件触发过载:鼠标移动时会持续调用activity函数,短时间内产生大量冗余执行。
  3. 事件捕获模式无意义:监听事件时使用捕获模式(第三个参数true),增加了事件处理的额外开销,实际用默认的冒泡模式即可满足需求。
  4. 无资源清理逻辑:页面卸载时,定时器和事件监听器未被清理,可能引发内存泄漏。

优化方案及代码实现

  • 用setTimeout替代setInterval:用户活跃时重置超时,仅在用户进入闲置状态后才触发检查,减少无效执行。
  • 精简监听事件:移除高频触发的mousemove,改用mouseenter或对mousemove做防抖处理;保留mousedown、keydown、scroll、touchstart这类明确的用户交互事件。
  • 对activity函数做防抖处理,避免短时间内多次调用。
  • 添加页面卸载时的资源清理逻辑,清除定时器和事件监听器。

优化后的代码:

function activityWatcher() {
    let timeoutId = null;
    const maxInactivity = 30; // 闲置阈值(秒)
    let lastActivityTime = Date.now();

    // 检查闲置状态的函数
    function checkInactivity() {
        const elapsedSeconds = Math.floor((Date.now() - lastActivityTime) / 1000);
        console.log(`${elapsedSeconds} seconds since the user was last active`);
        
        if (elapsedSeconds > maxInactivity) {
            console.log(`User has been inactive for more than ${maxInactivity} seconds`);
            // 这里可触发闲置后的业务逻辑,比如上报闲置状态
        } else {
            // 未达到闲置阈值,继续监听
            timeoutId = setTimeout(checkInactivity, 1000);
        }
    }

    // 防抖处理的用户活跃回调
    const activity = debounce(() => {
        lastActivityTime = Date.now();
        // 重置定时器
        if (timeoutId) clearTimeout(timeoutId);
        timeoutId = setTimeout(checkInactivity, 1000);
    }, 200); // 200ms防抖间隔,避免高频触发

    // 防抖工具函数
    function debounce(func, delay) {
        let debounceId;
        return function() {
            clearTimeout(debounceId);
            debounceId = setTimeout(func, delay);
        };
    }

    // 监听的用户活动事件
    const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
    activityEvents.forEach(eventName => {
        document.addEventListener(eventName, activity);
    });

    // 页面卸载时清理资源
    window.addEventListener('beforeunload', () => {
        if (timeoutId) clearTimeout(timeoutId);
        activityEvents.forEach(eventName => {
            document.removeEventListener(eventName, activity);
        });
    });

    // 启动监听
    timeoutId = setTimeout(checkInactivity, 1000);
}

activityWatcher();

二、获取用户进入和离开页面的时间

1. 记录进入页面时间

页面加载阶段即可记录,推荐使用window.performance API获取精准的导航开始时间,或在DOM加载完成时记录当前时间:

// 方式1:获取页面开始加载的精准时间(毫秒级时间戳)
const pageEnterTime = window.performance.timing.navigationStart;

// 方式2:DOM加载完成时记录当前时间(ISO格式字符串)
document.addEventListener('DOMContentLoaded', () => {
    const pageEnterTime = new Date().toISOString();
    // 存储到变量,后续用于计算时长或上报
});

2. 记录离开页面时间

监听beforeunload(页面卸载前触发)和visibilitychange(页面切换标签、最小化时触发)事件,覆盖不同场景的离开行为:

let pageLeaveTime = null;

// 页面卸载时记录离开时间
window.addEventListener('beforeunload', () => {
    pageLeaveTime = new Date().toISOString();
    // 触发数据上报逻辑
});

// 页面隐藏时记录(比如切换到其他标签页)
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
        pageLeaveTime = new Date().toISOString();
        // 上报页面隐藏的时间
    }
});

三、高效保存用户活动数据到数据库

前端优化点

  1. 批量提交:不要每次状态变化都发送请求,积累多次活动数据或在页面离开时一次性提交,减少HTTP请求次数。
  2. 异步提交:使用fetch或axios发送异步POST请求,避免阻塞页面卸载;开启keepalive确保页面卸载时请求能完成。
  3. 失败重试:将未提交的失败数据存入localStorage,下次页面加载时再次尝试提交。

前端提交示例:

// 组装用户活动数据
const userActivityData = {
    userId: '当前用户ID', // 从后端获取或本地存储
    pageUrl: window.location.href,
    enterTime: new Date(pageEnterTime).toISOString(),
    leaveTime: pageLeaveTime,
    duration: Math.floor((new Date(pageLeaveTime) - new Date(pageEnterTime)) / 1000)
};

// 页面卸载时提交数据
window.addEventListener('beforeunload', async () => {
    try {
        await fetch('/api/UserActivity/Record', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + localStorage.getItem('token') // 身份验证
            },
            body: JSON.stringify(userActivityData),
            keepalive: true // 确保页面卸载时请求能完成
        });
    } catch (error) {
        console.error('提交活动数据失败:', error);
        // 暂存数据到本地,下次加载时重试
        localStorage.setItem('pendingActivity', JSON.stringify(userActivityData));
    }
});

后端(ASP.NET)实现

  1. 设计表结构:创建UserActivities表,字段示例:

    • Id(主键,自增)
    • UserId(外键,关联用户表)
    • PageUrl(nvarchar)
    • EnterTime(datetime)
    • LeaveTime(datetime)
    • Duration(int,秒)
    • CreatedTime(datetime,默认当前时间)
  2. 编写API接口:使用ASP.NET Core Web API接收前端数据,支持单条或批量插入:

[ApiController]
[Route("api/[controller]")]
public class UserActivityController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public UserActivityController(ApplicationDbContext context)
    {
        _context = context;
    }

    // 单条记录提交
    [HttpPost("Record")]
    public async Task<IActionResult> RecordActivity([FromBody] UserActivityDto activityDto)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);

        var activity = new UserActivity
        {
            UserId = activityDto.UserId,
            PageUrl = activityDto.PageUrl,
            EnterTime = DateTime.Parse(activityDto.EnterTime),
            LeaveTime = DateTime.Parse(activityDto.LeaveTime),
            Duration = activityDto.Duration
        };

        _context.UserActivities.Add(activity);
        await _context.SaveChangesAsync();

        return Ok();
    }

    // 批量记录提交(可选)
    [HttpPost("BatchRecord")]
    public async Task<IActionResult> BatchRecord([FromBody] List<UserActivityDto> activityDtos)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);

        var activities = activityDtos.Select(dto => new UserActivity
        {
            UserId = dto.UserId,
            PageUrl = dto.PageUrl,
            EnterTime = DateTime.Parse(dto.EnterTime),
            LeaveTime = DateTime.Parse(dto.LeaveTime),
            Duration = dto.Duration
        });

        _context.UserActivities.AddRange(activities);
        await _context.SaveChangesAsync();

        return Ok();
    }
}

// DTO类
public class UserActivityDto
{
    public string UserId { get; set; }
    public string PageUrl { get; set; }
    public string EnterTime { get; set; }
    public string LeaveTime { get; set; }
    public int Duration { get; set; }
}
  1. 后端性能优化:
    • 使用EF Core第三方库(如EFCore.BulkExtensions)实现高效批量插入。
    • 开启数据库连接池,减少连接创建开销。
    • 异步处理请求,避免线程阻塞。
    • 数据量较大时,用Redis暂存数据,定时批量写入数据库,降低数据库压力。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 16:45:29