Skip to content
📖 14 min🎯Difficulty:
Prerequisites:Lazy LoadingMemory Optimization

The Problem

Your shell feels sluggish. Animations stutter, popups take time to appear, and CPU usage spikes when the clock ticks. Without profiling, you're guessing at the cause.

The Naive Approach

Add more console.log statements:

qml
console.log("Animation started: " + Date.now())

Logging timestamps gives a rough idea but doesn't identify rendering bottlenecks, layout passes, or binding evaluation costs.

💡Mental Model

Think of profiling as a mechanic's diagnostic tool. The engine sputters. You could guess it's the spark plugs, fuel filter, or oxygen sensor. Or you can plug in an OBD-II scanner and get exact error codes. Profiling is the OBD-II scanner for your shell.

Profiling Tools

1. Qt Quick Profiler

bash
# Start with profiling enabled
quickshell --qmljsdebugger port:3768

# Connect from Qt Creator: Analyze > QML Profiler
# Or from command line with:
qmlprofiler -p 3768

2. Frame Rate Monitor

qml
// FpsMonitor.qml
Text {
  id: fpsText
  property real fps: 0
  property var frameTimes: []
  property int frameCount: 0
  color: fps >= 55 ? "#a6e3a1" : fps >= 30 ? "#fab387" : "#f38ba8"
  font.pixelSize: 11

  Timer {
    interval: 1000; running: true; repeat: true
    onTriggered: {
      var avg = fpsText.frameTimes.reduce(function(a,b) { return a+b }, 0) / fpsText.frameTimes.length
      fpsText.fps = avg > 0 ? 1000 / avg : 0
      fpsText.text = fpsText.fps.toFixed(0) + " FPS"
      fpsText.frameTimes = []
    }
  }

  // Track frame rendering time
  Connections {
    target: fpsText
    function onFrameChanged() {
      var now = Date.now()
      if (fpsText.frameTimes.length > 0) {
        var elapsed = now - fpsText.frameTimes[fpsText.frameTimes.length - 1].timestamp
        fpsText.frameTimes.push(elapsed)
      }
      fpsText.frameTimes.push({ timestamp: now })
      if (fpsText.frameTimes.length > 60) fpsText.frameTimes.shift()
    }
  }
}

3. Frame Recording with Quickshell

qml
// ProfileCapture.qml
QtObject {
  id: profiler

  property var frames: []
  property bool recording: false

  function startRecording() {
    frames = []
    recording = true
    captureFrame()
  }

  function stopRecording() {
    recording = false
    return analyzeFrames()
  }

  function captureFrame() {
    if (!recording) return
    frames.push({
      timestamp: Date.now(),
      cpuUsage: CpuService.usagePercent,
      memoryUsage: processMemoryUsage()
    })
    Timer.callLater(captureFrame)
  }

  function analyzeFrames() {
    var result = {
      totalFrames: frames.length,
      avgCpu: frames.reduce(function(s, f) { return s + f.cpuUsage }, 0) / frames.length,
      duration: frames.length > 1 ? frames[frames.length-1].timestamp - frames[0].timestamp : 0
    }
    return result
  }
}

Identifying Bottlenecks

Binding Overhead

qml
// Bad: expensive expression evaluated on every change
Text { text: parent.width * parent.height / CpuService.usagePercent / 100 * 2 }

// Good: precompute in a property
property int computed: parent.width * parent.height
Text { text: computed / CpuService.usagePercent / 100 * 2 }

Layout Passes

qml
// Bad: triggers relayout on every pixel change
Anchors { left: parent.left; right: parent.right; leftMargin: someAnimatedProperty }

// Good: use transforms instead of changing margins
Transform.translate: Translate { x: someAnimatedProperty }

Expensive Delegates

qml
// Bad: each delegate creates a heavy component tree
delegate: Column {
  Rectangle { width: 200; height: 200; Image { source: modelData.largeIcon } }
  Text { text: modelData.name }
  Text { text: modelData.description } // Rarely visible
}

// Good: simplify delegate, show details on demand
delegate: Row {
  Rectangle { width: 40; height: 40; Image { source: modelData.thumbnail; sourceSize.width: 40 } }
  Text { text: modelData.name }
}

Optimization Checklist

CheckToolAction
Frame rate < 55FpsMonitorSimplify animations, reduce overdraw
Binding evaluation > 1msQML ProfilerCache expressions, use property bindings
Memory growthps/rssFix leaks, add lazy loading
Layout passes > 10/sQML ProfilerUse fixed sizes, avoid anchors in animations
Image loadingQt Quick InspectorSet sourceSize, use asynchronous

Exercises

Exercise
Add the FpsMonitor to your shell. Run it for 10 minutes while using your computer. Record the minimum and average FPS. Identify the top 3 frame drops and their causes.
⭐⭐Exercise
Use Qt Creator's QML Profiler to record a 30-second session. Find the function with the longest binding evaluation time. Optimize it using caching or alternative expression.
⭐⭐⭐Exercise
Build a performance dashboard that logs FPS, CPU, and memory every second. Export to CSV. Analyze trends over a 1-hour session. Identify memory leaks (steadily increasing memory without corresponding usage increase).
What You've Learned
  • Use Qt Quick Profiler and QML Profiler in Qt Creator for deep analysis
  • Add an FPS monitor overlay to detect stuttering in real-time
  • Avoid expensive bindings — cache intermediate computations as properties
  • Use transforms instead of anchor margins for animated properties
  • Simplify delegates in ListView/GridView to reduce per-item overhead

Built with VitePress. Licensed under MIT.