Skip to main content
Glama

runPerformanceAudit

Analyze webpage performance metrics to identify loading issues and optimization opportunities for improved user experience.

Instructions

Run a performance audit on the current page

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • Core handler function that executes the performance audit by running Lighthouse on the PERFORMANCE category and processing results with extractAIOptimizedData
    export async function runPerformanceAudit(
      url: string
    ): Promise<AIOptimizedPerformanceReport> {
      try {
        const lhr = await runLighthouseAudit(url, [AuditCategory.PERFORMANCE]);
        return extractAIOptimizedData(lhr, url);
      } catch (error) {
        throw new Error(
          `Performance audit failed: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
      }
    }
  • Helper function that processes raw Lighthouse results into AI-optimized performance report format, extracting key metrics, opportunities, page stats, and recommendations
    const extractAIOptimizedData = (
      lhr: LighthouseResult,
      url: string
    ): AIOptimizedPerformanceReport => {
      const audits = lhr.audits || {};
      const categoryData = lhr.categories[AuditCategory.PERFORMANCE];
      const score = Math.round((categoryData?.score || 0) * 100);
    
      // Add metadata
      const metadata = {
        url,
        timestamp: lhr.fetchTime || new Date().toISOString(),
        device: "desktop", // This could be made configurable
        lighthouseVersion: lhr.lighthouseVersion,
      };
    
      // Count audits by type
      const auditRefs = categoryData?.auditRefs || [];
      let failedCount = 0;
      let passedCount = 0;
      let manualCount = 0;
      let informativeCount = 0;
      let notApplicableCount = 0;
    
      auditRefs.forEach((ref) => {
        const audit = audits[ref.id];
        if (!audit) return;
    
        if (audit.scoreDisplayMode === "manual") {
          manualCount++;
        } else if (audit.scoreDisplayMode === "informative") {
          informativeCount++;
        } else if (audit.scoreDisplayMode === "notApplicable") {
          notApplicableCount++;
        } else if (audit.score !== null) {
          if (audit.score >= 0.9) {
            passedCount++;
          } else {
            failedCount++;
          }
        }
      });
    
      const audit_counts = {
        failed: failedCount,
        passed: passedCount,
        manual: manualCount,
        informative: informativeCount,
        not_applicable: notApplicableCount,
      };
    
      const metrics: AIOptimizedMetric[] = [];
      const opportunities: AIOptimizedOpportunity[] = [];
    
      // Extract core metrics
      if (audits["largest-contentful-paint"]) {
        const lcp = audits["largest-contentful-paint"];
        const lcpElement = audits["largest-contentful-paint-element"];
    
        const metric: AIOptimizedMetric = {
          id: "lcp",
          score: lcp.score,
          value_ms: Math.round(lcp.numericValue || 0),
          passes_core_web_vital: lcp.score !== null && lcp.score >= 0.9,
        };
    
        // Enhanced LCP element detection
    
        // 1. Try from largest-contentful-paint-element audit
        if (lcpElement && lcpElement.details) {
          const lcpDetails = lcpElement.details as any;
    
          // First attempt - try to get directly from items
          if (
            lcpDetails.items &&
            Array.isArray(lcpDetails.items) &&
            lcpDetails.items.length > 0
          ) {
            const item = lcpDetails.items[0];
    
            // For text elements in tables format
            if (item.type === "table" && item.items && item.items.length > 0) {
              const firstTableItem = item.items[0];
    
              if (firstTableItem.node) {
                if (firstTableItem.node.selector) {
                  metric.element_selector = firstTableItem.node.selector;
                }
    
                // Determine element type based on path or selector
                const path = firstTableItem.node.path;
                const selector = firstTableItem.node.selector || "";
    
                if (path) {
                  if (
                    selector.includes(" > img") ||
                    selector.includes(" img") ||
                    selector.endsWith("img") ||
                    path.includes(",IMG")
                  ) {
                    metric.element_type = "image";
    
                    // Try to extract image name from selector
                    const imgMatch = selector.match(/img[.][^> ]+/);
                    if (imgMatch && !metric.element_url) {
                      metric.element_url = imgMatch[0];
                    }
                  } else if (
                    path.includes(",SPAN") ||
                    path.includes(",P") ||
                    path.includes(",H")
                  ) {
                    metric.element_type = "text";
                  }
                }
    
                // Try to extract text content if available
                if (firstTableItem.node.nodeLabel) {
                  metric.element_content = firstTableItem.node.nodeLabel.substring(
                    0,
                    100
                  );
                }
              }
            }
            // Original handling for direct items
            else if (item.node?.nodeLabel) {
              // Determine element type from node label
              if (item.node.nodeLabel.startsWith("<img")) {
                metric.element_type = "image";
                // Try to extract image URL from the node snippet
                const match = item.node.snippet?.match(/src="([^"]+)"/);
                if (match && match[1]) {
                  metric.element_url = match[1];
                }
              } else if (item.node.nodeLabel.startsWith("<video")) {
                metric.element_type = "video";
              } else if (item.node.nodeLabel.startsWith("<h")) {
                metric.element_type = "heading";
              } else {
                metric.element_type = "text";
              }
    
              if (item.node?.selector) {
                metric.element_selector = item.node.selector;
              }
            }
          }
        }
    
        // 2. Try from lcp-lazy-loaded audit
        const lcpImageAudit = audits["lcp-lazy-loaded"];
        if (lcpImageAudit && lcpImageAudit.details) {
          const lcpImageDetails = lcpImageAudit.details as any;
    
          if (
            lcpImageDetails.items &&
            Array.isArray(lcpImageDetails.items) &&
            lcpImageDetails.items.length > 0
          ) {
            const item = lcpImageDetails.items[0];
    
            if (item.url) {
              metric.element_type = "image";
              metric.element_url = item.url;
            }
          }
        }
    
        // 3. Try directly from the LCP audit details
        if (!metric.element_url && lcp.details) {
          const lcpDirectDetails = lcp.details as any;
    
          if (lcpDirectDetails.items && Array.isArray(lcpDirectDetails.items)) {
            for (const item of lcpDirectDetails.items) {
              if (item.url || (item.node && item.node.path)) {
                if (item.url) {
                  metric.element_url = item.url;
                  metric.element_type = item.url.match(
                    /\.(jpg|jpeg|png|gif|webp|svg)$/i
                  )
                    ? "image"
                    : "resource";
                }
                if (item.node && item.node.selector) {
                  metric.element_selector = item.node.selector;
                }
                break;
              }
            }
          }
        }
    
        // 4. Check for specific audit that might contain image info
        const largestImageAudit = audits["largest-image-paint"];
        if (largestImageAudit && largestImageAudit.details) {
          const imageDetails = largestImageAudit.details as any;
    
          if (
            imageDetails.items &&
            Array.isArray(imageDetails.items) &&
            imageDetails.items.length > 0
          ) {
            const item = imageDetails.items[0];
    
            if (item.url) {
              // If we have a large image that's close in time to LCP, it's likely the LCP element
              metric.element_type = "image";
              metric.element_url = item.url;
            }
          }
        }
    
        // 5. Check for network requests audit to find image resources
        if (!metric.element_url) {
          const networkRequests = audits["network-requests"];
    
          if (networkRequests && networkRequests.details) {
            const networkDetails = networkRequests.details as any;
    
            if (networkDetails.items && Array.isArray(networkDetails.items)) {
              // Get all image resources loaded close to the LCP time
              const lcpTime = lcp.numericValue || 0;
              const imageResources = networkDetails.items
                .filter(
                  (item: any) =>
                    item.url &&
                    item.mimeType &&
                    item.mimeType.startsWith("image/") &&
                    item.endTime &&
                    Math.abs(item.endTime - lcpTime) < 500 // Within 500ms of LCP
                )
                .sort(
                  (a: any, b: any) =>
                    Math.abs(a.endTime - lcpTime) - Math.abs(b.endTime - lcpTime)
                );
    
              if (imageResources.length > 0) {
                const closestImage = imageResources[0];
    
                if (!metric.element_type) {
                  metric.element_type = "image";
                  metric.element_url = closestImage.url;
                }
              }
            }
          }
        }
    
        metrics.push(metric);
      }
    
      if (audits["first-contentful-paint"]) {
        const fcp = audits["first-contentful-paint"];
        metrics.push({
          id: "fcp",
          score: fcp.score,
          value_ms: Math.round(fcp.numericValue || 0),
          passes_core_web_vital: fcp.score !== null && fcp.score >= 0.9,
        });
      }
    
      if (audits["speed-index"]) {
        const si = audits["speed-index"];
        metrics.push({
          id: "si",
          score: si.score,
          value_ms: Math.round(si.numericValue || 0),
        });
      }
    
      if (audits["interactive"]) {
        const tti = audits["interactive"];
        metrics.push({
          id: "tti",
          score: tti.score,
          value_ms: Math.round(tti.numericValue || 0),
        });
      }
    
      // Add CLS (Cumulative Layout Shift)
      if (audits["cumulative-layout-shift"]) {
        const cls = audits["cumulative-layout-shift"];
        metrics.push({
          id: "cls",
          score: cls.score,
          // CLS is not in ms, but a unitless value
          value_ms: Math.round((cls.numericValue || 0) * 1000) / 1000, // Convert to 3 decimal places
          passes_core_web_vital: cls.score !== null && cls.score >= 0.9,
        });
      }
    
      // Add TBT (Total Blocking Time)
      if (audits["total-blocking-time"]) {
        const tbt = audits["total-blocking-time"];
        metrics.push({
          id: "tbt",
          score: tbt.score,
          value_ms: Math.round(tbt.numericValue || 0),
          passes_core_web_vital: tbt.score !== null && tbt.score >= 0.9,
        });
      }
    
      // Extract opportunities
      if (audits["render-blocking-resources"]) {
        const rbrAudit = audits["render-blocking-resources"];
    
        // Determine impact level based on potential savings
        let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
        const savings = Math.round(rbrAudit.numericValue || 0);
    
        if (savings > 2000) {
          impact = "critical";
        } else if (savings > 1000) {
          impact = "serious";
        } else if (savings < 300) {
          impact = "minor";
        }
    
        const opportunity: AIOptimizedOpportunity = {
          id: "render_blocking_resources",
          savings_ms: savings,
          severity: impact,
          resources: [],
        };
    
        const rbrDetails = rbrAudit.details as any;
        if (rbrDetails && rbrDetails.items && Array.isArray(rbrDetails.items)) {
          // Determine how many items to include based on impact
          const itemLimit = DETAIL_LIMITS[impact];
    
          rbrDetails.items
            .slice(0, itemLimit)
            .forEach((item: { url?: string; wastedMs?: number }) => {
              if (item.url) {
                // Extract file name from full URL
                const fileName = item.url.split("/").pop() || item.url;
                opportunity.resources.push({
                  url: fileName,
                  savings_ms: Math.round(item.wastedMs || 0),
                });
              }
            });
        }
    
        if (opportunity.resources.length > 0) {
          opportunities.push(opportunity);
        }
      }
    
      if (audits["uses-http2"]) {
        const http2Audit = audits["uses-http2"];
    
        // Determine impact level based on potential savings
        let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
        const savings = Math.round(http2Audit.numericValue || 0);
    
        if (savings > 2000) {
          impact = "critical";
        } else if (savings > 1000) {
          impact = "serious";
        } else if (savings < 300) {
          impact = "minor";
        }
    
        const opportunity: AIOptimizedOpportunity = {
          id: "http2",
          savings_ms: savings,
          severity: impact,
          resources: [],
        };
    
        const http2Details = http2Audit.details as any;
        if (
          http2Details &&
          http2Details.items &&
          Array.isArray(http2Details.items)
        ) {
          // Determine how many items to include based on impact
          const itemLimit = DETAIL_LIMITS[impact];
    
          http2Details.items
            .slice(0, itemLimit)
            .forEach((item: { url?: string }) => {
              if (item.url) {
                // Extract file name from full URL
                const fileName = item.url.split("/").pop() || item.url;
                opportunity.resources.push({ url: fileName });
              }
            });
        }
    
        if (opportunity.resources.length > 0) {
          opportunities.push(opportunity);
        }
      }
    
      // After extracting all metrics and opportunities, collect page stats
      // Extract page stats
      let page_stats: AIPageStats | undefined;
    
      // Total page stats
      const totalByteWeight = audits["total-byte-weight"];
      const networkRequests = audits["network-requests"];
      const thirdPartyAudit = audits["third-party-summary"];
      const mainThreadWork = audits["mainthread-work-breakdown"];
    
      if (networkRequests && networkRequests.details) {
        const resourceDetails = networkRequests.details as any;
    
        if (resourceDetails.items && Array.isArray(resourceDetails.items)) {
          const resources = resourceDetails.items;
          const totalRequests = resources.length;
    
          // Calculate total size and counts by type
          let totalSizeKb = 0;
          let jsCount = 0,
            cssCount = 0,
            imgCount = 0,
            fontCount = 0,
            otherCount = 0;
    
          resources.forEach((resource: any) => {
            const sizeKb = resource.transferSize
              ? Math.round(resource.transferSize / 1024)
              : 0;
            totalSizeKb += sizeKb;
    
            // Count by mime type
            const mimeType = resource.mimeType || "";
            if (mimeType.includes("javascript") || resource.url.endsWith(".js")) {
              jsCount++;
            } else if (mimeType.includes("css") || resource.url.endsWith(".css")) {
              cssCount++;
            } else if (
              mimeType.includes("image") ||
              /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(resource.url)
            ) {
              imgCount++;
            } else if (
              mimeType.includes("font") ||
              /\.(woff|woff2|ttf|otf|eot)$/i.test(resource.url)
            ) {
              fontCount++;
            } else {
              otherCount++;
            }
          });
    
          // Calculate third-party size
          let thirdPartySizeKb = 0;
          if (thirdPartyAudit && thirdPartyAudit.details) {
            const thirdPartyDetails = thirdPartyAudit.details as any;
            if (thirdPartyDetails.items && Array.isArray(thirdPartyDetails.items)) {
              thirdPartyDetails.items.forEach((item: any) => {
                if (item.transferSize) {
                  thirdPartySizeKb += Math.round(item.transferSize / 1024);
                }
              });
            }
          }
    
          // Get main thread blocking time
          let mainThreadBlockingTimeMs = 0;
          if (mainThreadWork && mainThreadWork.numericValue) {
            mainThreadBlockingTimeMs = Math.round(mainThreadWork.numericValue);
          }
    
          // Create page stats object
          page_stats = {
            total_size_kb: totalSizeKb,
            total_requests: totalRequests,
            resource_counts: {
              js: jsCount,
              css: cssCount,
              img: imgCount,
              font: fontCount,
              other: otherCount,
            },
            third_party_size_kb: thirdPartySizeKb,
            main_thread_blocking_time_ms: mainThreadBlockingTimeMs,
          };
        }
      }
    
      // Generate prioritized recommendations
      const prioritized_recommendations: string[] = [];
    
      // Add key recommendations based on failed audits with high impact
      if (
        audits["render-blocking-resources"] &&
        audits["render-blocking-resources"].score !== null &&
        audits["render-blocking-resources"].score === 0
      ) {
        prioritized_recommendations.push("Eliminate render-blocking resources");
      }
    
      if (
        audits["uses-responsive-images"] &&
        audits["uses-responsive-images"].score !== null &&
        audits["uses-responsive-images"].score === 0
      ) {
        prioritized_recommendations.push("Properly size images");
      }
    
      if (
        audits["uses-optimized-images"] &&
        audits["uses-optimized-images"].score !== null &&
        audits["uses-optimized-images"].score === 0
      ) {
        prioritized_recommendations.push("Efficiently encode images");
      }
    
      if (
        audits["uses-text-compression"] &&
        audits["uses-text-compression"].score !== null &&
        audits["uses-text-compression"].score === 0
      ) {
        prioritized_recommendations.push("Enable text compression");
      }
    
      if (
        audits["uses-http2"] &&
        audits["uses-http2"].score !== null &&
        audits["uses-http2"].score === 0
      ) {
        prioritized_recommendations.push("Use HTTP/2");
      }
    
      // Add more specific recommendations based on Core Web Vitals
      if (
        audits["largest-contentful-paint"] &&
        audits["largest-contentful-paint"].score !== null &&
        audits["largest-contentful-paint"].score < 0.5
      ) {
        prioritized_recommendations.push("Improve Largest Contentful Paint (LCP)");
      }
    
      if (
        audits["cumulative-layout-shift"] &&
        audits["cumulative-layout-shift"].score !== null &&
        audits["cumulative-layout-shift"].score < 0.5
      ) {
        prioritized_recommendations.push("Reduce layout shifts (CLS)");
      }
    
      if (
        audits["total-blocking-time"] &&
        audits["total-blocking-time"].score !== null &&
        audits["total-blocking-time"].score < 0.5
      ) {
        prioritized_recommendations.push("Reduce JavaScript execution time");
      }
    
      // Create the performance report content
      const reportContent: PerformanceReportContent = {
        score,
        audit_counts,
        metrics,
        opportunities,
        page_stats,
        prioritized_recommendations:
          prioritized_recommendations.length > 0
            ? prioritized_recommendations
            : undefined,
      };
    
      // Return the full report following the LighthouseReport interface
      return {
        metadata,
        report: reportContent,
      };
    };
  • MCP server tool registration for runPerformanceAudit, proxies request to browser-tools-server /performance-audit endpoint
    server.tool(
      "runPerformanceAudit",
      "Run a performance audit on the current page",
      {},
      async () => {
        return await withServerConnection(async () => {
          try {
            // Simplified approach - let the browser connector handle the current tab and URL
            console.log(
              `Sending POST request to http://${discoveredHost}:${discoveredPort}/performance-audit`
            );
            const response = await fetch(
              `http://${discoveredHost}:${discoveredPort}/performance-audit`,
              {
                method: "POST",
                headers: {
                  "Content-Type": "application/json",
                  Accept: "application/json",
                },
                body: JSON.stringify({
                  category: AuditCategory.PERFORMANCE,
                  source: "mcp_tool",
                  timestamp: Date.now(),
                }),
              }
            );
    
            // Log the response status
            console.log(`Performance audit response status: ${response.status}`);
    
            if (!response.ok) {
              const errorText = await response.text();
              console.error(`Performance audit error: ${errorText}`);
              throw new Error(`Server returned ${response.status}: ${errorText}`);
            }
    
            const json = await response.json();
    
            // flatten it by merging metadata with the report contents
            if (json.report) {
              const { metadata, report } = json;
              const flattened = {
                ...metadata,
                ...report,
              };
    
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify(flattened, null, 2),
                  },
                ],
              };
            } else {
              // Return as-is if it's not in the new format
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify(json, null, 2),
                  },
                ],
              };
            }
          } catch (error) {
            const errorMessage =
              error instanceof Error ? error.message : String(error);
            console.error("Error in performance audit:", errorMessage);
            return {
              content: [
                {
                  type: "text",
                  text: `Failed to run performance audit: ${errorMessage}`,
                },
              ],
            };
          }
        });
      }
    );
  • Registers the /performance-audit HTTP POST endpoint in Express server, imports and calls runPerformanceAudit from lighthouse/index.js
    // Sets up the performance audit endpoint
    private setupPerformanceAudit() {
      this.setupAuditEndpoint(
        AuditCategory.PERFORMANCE,
        "/performance-audit",
        runPerformanceAudit
      );
    }
  • TypeScript interfaces defining the input/output schema for the performance audit report, optimized for AI consumption
    export interface PerformanceReportContent {
      score: number; // Overall score (0-100)
      audit_counts: {
        // Counts of different audit types
        failed: number;
        passed: number;
        manual: number;
        informative: number;
        not_applicable: number;
      };
      metrics: AIOptimizedMetric[];
      opportunities: AIOptimizedOpportunity[];
      page_stats?: AIPageStats; // Optional page statistics
      prioritized_recommendations?: string[]; // Ordered list of recommendations
    }
    
    /**
     * Full performance report implementing the base LighthouseReport interface
     */
    export type AIOptimizedPerformanceReport =
      LighthouseReport<PerformanceReportContent>;
    
    // AI-optimized performance metric format
    interface AIOptimizedMetric {
      id: string; // Short ID like "lcp", "fcp"
      score: number | null; // 0-1 score
      value_ms: number; // Value in milliseconds
      element_type?: string; // For LCP: "image", "text", etc.
      element_selector?: string; // DOM selector for the element
      element_url?: string; // For images/videos
      element_content?: string; // For text content (truncated)
      passes_core_web_vital?: boolean; // Whether this metric passes as a Core Web Vital
    }
    
    // AI-optimized opportunity format
    interface AIOptimizedOpportunity {
      id: string; // Like "render_blocking", "http2"
      savings_ms: number; // Time savings in ms
      severity?: "critical" | "serious" | "moderate" | "minor"; // Severity classification
      resources: Array<{
        url: string; // Resource URL
        savings_ms?: number; // Individual resource savings
        size_kb?: number; // Size in KB
        type?: string; // Resource type (js, css, img, etc.)
        is_third_party?: boolean; // Whether this is a third-party resource
      }>;
    }
    
    // Page stats for AI analysis
    interface AIPageStats {
      total_size_kb: number; // Total page weight in KB
      total_requests: number; // Total number of requests
      resource_counts: {
        // Count by resource type
        js: number;
        css: number;
        img: number;
        font: number;
        other: number;
      };
      third_party_size_kb: number; // Size of third-party resources
      main_thread_blocking_time_ms: number; // Time spent blocking the main thread
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It states the action but doesn't disclose behavioral traits such as what the audit entails, whether it's destructive, if it requires specific permissions, or what the output looks like. For a tool with zero annotation coverage, this leaves significant gaps in understanding its operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, clear sentence with zero waste. It's front-loaded and efficiently conveys the core purpose without extra verbiage, making it easy to parse and understand immediately.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the lack of annotations and output schema, the description is incomplete. It doesn't explain what a 'performance audit' entails, what results to expect, or how it differs from other audit tools. For a tool with no structured data to fall back on, more context is needed to make it actionable for an agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 0 parameters with 100% coverage, so no parameter documentation is needed. The description doesn't add parameter details, which is appropriate here. A baseline of 4 is applied since there are no parameters to document, and the description doesn't introduce unnecessary complexity.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('run a performance audit') and target ('on the current page'), which is specific and unambiguous. However, it doesn't differentiate from sibling audit tools like 'runAccessibilityAudit' or 'runSEOAudit'β€”it merely states what it does without explaining how it differs from other audit types.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus alternatives. The description lacks context about prerequisites (e.g., whether a page must be loaded), exclusions, or comparisons to siblings like 'runBestPracticesAudit' or 'runAuditMode'. It's a standalone statement with no usage instructions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/oenius/browser-tools-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server