-
-
Notifications
You must be signed in to change notification settings - Fork 62
OPTIMIZATION
Best practices for optimizing iNiR based on Qt6 QML documentation and KDAB recommendations.
| Do | Don't |
|---|---|
property int size: 10 |
property var size: 10 |
visible: false |
opacity: 0 |
anchors.fill: parent |
width: parent.width; height: parent.height |
root.myProperty (qualified) |
myProperty (unqualified) |
asynchronous: true on images |
Sync image loading |
| Cache lookups in loops | Repeated property access |
Always use concrete types instead of var:
// Bad
property var size: 10
property var items: []
// Good
property int size: 10
property list<string> items: []Annotate function parameters and return types:
// Bad
function calculate(width, height) {
return width * height
}
// Good
function calculate(width: real, height: real): real {
return width * height
}Always qualify property access with object id:
Item {
id: root
property int size: 10
Rectangle {
// Bad - unqualified lookup
width: size
// Good - qualified lookup
width: root.size
}
}Cache property lookups outside tight loops:
// Bad - resolves rect.color 4 times per iteration
for (var i = 0; i < 1000; ++i) {
printValue("red", rect.color.r)
printValue("green", rect.color.g)
printValue("blue", rect.color.b)
printValue("alpha", rect.color.a)
}
// Good - resolve once, use cached value
var rectColor = rect.color
for (var i = 0; i < 1000; ++i) {
printValue("red", rectColor.r)
printValue("green", rectColor.g)
printValue("blue", rectColor.b)
printValue("alpha", rectColor.a)
}Use temporary accumulators to avoid intermediate re-evaluations:
// Bad - triggers 6 binding re-evaluations
for (var i = 0; i < someData.length; ++i) {
accumulatedValue = accumulatedValue + someData[i]
}
// Good - single re-evaluation at the end
var temp = accumulatedValue
for (var i = 0; i < someData.length; ++i) {
temp = temp + someData[i]
}
accumulatedValue = tempUse visible: false instead of opacity: 0:
// Bad - still renders, just transparent
opacity: 0
// Good - skips rendering entirely
visible: falsePrefer anchors for relative positioning:
// Bad - binding-based positioning
Rectangle {
x: rect1.x
y: rect1.y + rect1.height
width: rect1.width - 20
}
// Good - anchor-based positioning
Rectangle {
anchors.left: rect1.left
anchors.top: rect1.bottom
anchors.right: rect1.right
anchors.rightMargin: 20
}Always use async loading and explicit source size:
Image {
source: "large-image.png"
asynchronous: true // Load in background thread
sourceSize: Qt.size(200, 200) // Scale before loading
cache: true // Cache decoded image
smooth: false // Disable if not needed
}Use simplest text format possible:
Text {
// Best performance
textFormat: Text.PlainText
// Only if you need basic formatting
// textFormat: Text.StyledText
// Avoid - expensive parsing
// textFormat: Text.AutoText
// textFormat: Text.RichText
}Keep delegates simple and avoid clipping:
ListView {
// Buffer delegates outside viewport
cacheBuffer: 200
delegate: Item {
// NEVER clip in delegates
// clip: true // BAD!
// Keep delegate simple
// Avoid ShaderEffects in delegates
}
}Use LazyLoader for panels not immediately needed:
// LazyLoader properties:
// - active: sync load, destroys on false
// - loading: starts async background load
// - activeAsync: async load, can read/write like active
// - item: accessing forces sync load if not ready
LazyLoader {
// Load when condition is true
active: Config.ready && someCondition
HeavyComponent {}
}Important: loading only STARTS async load, doesn't KEEP component active. Use active to maintain loaded state.
Avoid clip: true whenever possible:
// Bad - increases renderer complexity
Rectangle {
clip: true
// ...
}
// Good - use ClippingRectangle only when necessary
// Or restructure to avoid clippingiNiR's config uses Quickshell's JsonAdapter + FileView. Every property is declared in Config.qml with a typed default. When the user's config.json has a value, JsonAdapter reads it; when it doesn't, the schema default applies. The property always exists for declared schema keys.
// Config access — schema properties are guaranteed by JsonAdapter
property int value: Config.options.bar.cornerStyle // always valid
// ❌ Direct assignment — persists to disk via JsonAdapter, but does NOT emit
// configChanged(). Listeners (settings pages, bar layout, theme reactivity)
// will not update. This is the #1 silent bug source in iNiR.
// Config.options.bar.bottom = true
// ✅ Always use setNestedValue() — persists to disk AND emits configChanged()
// so every listener reacts correctly.
Config.setNestedValue("bar.bottom", true)
// Runtime data — may genuinely be null, USE optional chaining here
property string title: NiriService.activeWindow?.title ?? ""Project rule: always use
Config.setNestedValue("dot.path", value)for any config write. Direct property assignment (Config.options.x = y) skips theconfigChanged()signal — the value reaches disk but the UI and any reactive listeners never see the change.
Also: use
?.+??on config reads in module code. It protects against edge cases like key renames during migrations or malformed user configs. JsonAdapter guarantees schema defaults, but defensive access is the safer habit.
component PanelLoader: LazyLoader {
required property string identifier
property bool extraCondition: true
active: Config.ready && Config.options.enabledPanels.includes(identifier) && extraCondition
}Already optimized with asynchronous: true by default.
Use Appearance.animationsEnabled and Appearance.effectsEnabled to respect user preferences and GameMode.
- QML Profiler (Qt Creator): Find slow bindings and functions
- GammaRay: Analyze QML scenes
- Hotspot: CPU profiling
- Heaptrack: Memory profiling