[{"data":1,"prerenderedAt":1698},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance":3},{"id":4,"title":5,"body":6,"description":1688,"extension":1689,"meta":1690,"navigation":372,"path":1694,"seo":1695,"stem":1696,"__hash__":1697},"docs\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Findex.md","Background Tasks and Plugin Performance",{"type":7,"value":8,"toc":1673},"minimark",[9,13,17,36,171,176,199,203,209,217,220,336,340,676,705,711,715,718,732,746,752,835,839,846,883,906,913,917,920,933,946,956,1031,1051,1055,1069,1073,1076,1082,1088,1094,1097,1101,1104,1322,1339,1342,1346,1352,1355,1431,1443,1446,1453,1457,1519,1523,1529,1538,1547,1553,1566,1582,1599,1605,1615,1621,1625,1669],[10,11,5],"h1",{"id":12},"background-tasks-and-plugin-performance",[14,15,16],"p",{},"A plugin that freezes QGIS for forty seconds is, from the user's side, a plugin that has crashed. They click, nothing happens, the window greys out, the operating system offers to kill it. The work is progressing perfectly — it is simply happening on the thread that was supposed to be drawing the interface.",[14,18,19,20,25,26,30,31,35],{},"This guide sits inside ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development"," and covers the difference between a plugin that blocks and one that behaves. It starts from the plumbing established in ",[21,27,29],{"href":28},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate and Structure"," and ",[21,32,34],{"href":33},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces",", and adds the part that only shows up once real data arrives: doing the work somewhere else, telling the user how it is going, letting them stop it, and making it fast enough that they rarely need to.",[14,37,38],{},[39,40,45,49,53,60,77,86,93,102,108,113,117,120,124,129,134,140,144,149,153,158,160,163,167],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 292","img","The main thread drawing the interface while a long job blocks it, compared with the same job running on a task thread that reports progress and leaves the interface responsive","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"Blocking the main thread against running a task",[50,51,52],"desc",{},"In the upper timeline the interface thread starts a long job inline: repaints stop, the window is marked unresponsive, and only when the job ends does drawing resume. In the lower timeline the same job runs on a task thread while the interface thread keeps painting, receives progress signals and remains able to cancel.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","292","#f6f3ea",[61,62,63],"defs",{},[64,65,72],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"taskArrow","0 0 10 10","8","5","7","auto-start-reverse",[73,74],"path",{"d":75,"fill":76},"M0 0 L10 5 L0 10 z","#2f3b35",[78,79,85],"text",{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Same work, same duration, completely different experience",[78,87,92],{"x":88,"y":89,"style":90,"fill":91},"24","72","font-size:12px;font-weight:bold;font-family:sans-serif","#b91c1c","blocking",[54,94],{"x":95,"y":96,"width":97,"height":81,"rx":98,"fill":99,"stroke":100,"style":101},"140","56","80","4","#fffdf7","#59645f","stroke-width:1.5",[78,103,107],{"x":104,"y":105,"style":106,"fill":76,"textAnchor":84},"180","75","text-anchor:middle;font-size:10px;font-family:sans-serif","UI painting",[54,109],{"x":110,"y":96,"width":80,"height":81,"rx":98,"fill":111,"stroke":91,"style":112},"224","#fdf2e2","stroke-width:2.5",[78,114,116],{"x":115,"y":105,"style":106,"fill":91,"textAnchor":84},"414","40 s of work on the interface thread — window unresponsive",[54,118],{"x":119,"y":96,"width":97,"height":81,"rx":98,"fill":99,"stroke":100,"style":101},"608",[78,121,123],{"x":122,"y":105,"style":106,"fill":76,"textAnchor":84},"648","UI returns",[78,125,128],{"x":115,"y":126,"style":127,"fill":91,"textAnchor":84},"106","text-anchor:middle;font-size:11px;font-family:sans-serif","no progress, no cancel, no repaint — the user assumes it crashed",[78,130,133],{"x":88,"y":131,"style":90,"fill":132},"164","#15803d","QgsTask",[54,135],{"x":95,"y":136,"width":137,"height":81,"rx":98,"fill":138,"stroke":132,"style":139},"148","548","#edf8e9","stroke-width:2",[78,141,143],{"x":115,"y":142,"style":106,"fill":76,"textAnchor":84},"167","interface thread keeps painting, stays clickable",[54,145],{"x":110,"y":146,"width":80,"height":81,"rx":98,"fill":147,"stroke":148,"style":112},"196","#eef7f4","#0f766e",[78,150,152],{"x":115,"y":151,"style":106,"fill":76,"textAnchor":84},"215","task thread — the same 40 s of work",[154,155],"line",{"x1":156,"y1":146,"x2":156,"y2":104,"stroke":148,"style":157},"300","stroke-width:2;marker-end:url(#taskArrow)",[154,159],{"x1":115,"y1":146,"x2":115,"y2":104,"stroke":148,"style":157},[154,161],{"x1":162,"y1":146,"x2":162,"y2":104,"stroke":148,"style":157},"528",[78,164,166],{"x":115,"y":165,"style":127,"fill":132,"textAnchor":84},"248","progress signals travel up; a cancel request travels down",[78,168,170],{"x":80,"y":169,"style":127,"fill":100,"textAnchor":84},"278","The task manager owns the thread — you never create a QThread yourself",[172,173,175],"h2",{"id":174},"what-you-will-learn","What you will learn",[14,177,178,179,183,184,188,189,193,194,198],{},"Four pages sit under this guide. ",[21,180,182],{"href":181},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Long Task with QgsTask in PyQGIS"," is the core mechanism. ",[21,185,187],{"href":186},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002F","Show Progress and Support Cancellation in a QGIS Plugin"," makes it visible and interruptible. ",[21,190,192],{"href":191},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","Speed Up Feature Iteration with QgsFeatureRequest"," usually removes the need for a background task altogether by making the work ten times faster. ",[21,195,197],{"href":196},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fprofile-slow-pyqgis-code\u002F","Profile Slow PyQGIS Code"," tells you which of the three you actually need.",[172,200,202],{"id":201},"the-rule-that-governs-everything","The rule that governs everything",[14,204,205],{},[206,207,208],"strong",{},"Only the main thread may touch the interface, and only the main thread may safely touch layers registered in the project.",[14,210,211,212,216],{},"Qt is not thread-safe for widgets, and QGIS layer objects carry the same restriction: a ",[213,214,215],"code",{},"QgsVectorLayer"," in the project is owned by the main thread, and reading it from another thread while the canvas renders it is a genuine crash, not a warning. A background task may hold its own layer objects created inside the task, and may pass results back through signals — nothing more.",[14,218,219],{},"That single rule explains the shape of every pattern on these pages. Work is prepared on the main thread, a snapshot the task owns is handed over, the task computes, and the results come back to the main thread to be applied.",[14,221,222],{},[39,223,226,229,232,235,242,245,252,258,264,268,271,275,278,281,283,287,290,294,297,300,302,305,307,310,312,315,321,326,328,332],{"viewBox":224,"role":42,"ariaLabel":225,"xmlns":44},"0 0 760 280","Ownership diagram showing which objects belong to the main thread and which the task may own, with signals as the only channel between them",[46,227,228],{},"What each thread is allowed to touch",[50,230,231],{},"The main thread owns the interface widgets, the map canvas, the project and its registered layers. The task thread owns only what it created: its own feature source, plain Python data and its own layer copies. Signals are the only permitted channel between them, carrying progress upward and cancellation downward. Reading a project layer from the task thread is marked as a crash.",[54,233],{"x":56,"y":56,"width":57,"height":234,"fill":59},"280",[61,236,237],{},[64,238,240],{"id":239,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"ownArrow",[73,241],{"d":75,"fill":76},[78,243,244],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Cross this boundary with signals, never with pointers",[54,246],{"x":247,"y":248,"width":156,"height":146,"rx":249,"fill":250,"stroke":251,"style":112},"20","48","10","#eff3ff","#2563eb",[78,253,257],{"x":254,"y":255,"style":256,"fill":251,"textAnchor":84},"170","74","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","main thread owns",[54,259],{"x":260,"y":261,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},"44","88","252","30",[78,265,267],{"x":254,"y":266,"style":127,"fill":76,"textAnchor":84},"108","every widget and dialog",[54,269],{"x":260,"y":270,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},"124",[78,272,274],{"x":254,"y":273,"style":127,"fill":76,"textAnchor":84},"144","the map canvas",[54,276],{"x":260,"y":277,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},"160",[78,279,280],{"x":254,"y":104,"style":127,"fill":76,"textAnchor":84},"QgsProject and its layers",[54,282],{"x":260,"y":146,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},[78,284,286],{"x":254,"y":285,"style":127,"fill":76,"textAnchor":84},"216","applying the result",[54,288],{"x":289,"y":248,"width":156,"height":146,"rx":249,"fill":147,"stroke":148,"style":112},"440",[78,291,293],{"x":292,"y":255,"style":256,"fill":148,"textAnchor":84},"590","the task may own",[54,295],{"x":296,"y":261,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},"464",[78,298,299],{"x":292,"y":266,"style":127,"fill":76,"textAnchor":84},"a feature source snapshot",[54,301],{"x":296,"y":270,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},[78,303,304],{"x":292,"y":273,"style":127,"fill":76,"textAnchor":84},"plain Python data",[54,306],{"x":296,"y":277,"width":262,"height":263,"rx":69,"fill":99,"stroke":100,"style":101},[78,308,309],{"x":292,"y":104,"style":127,"fill":76,"textAnchor":84},"layers it created itself",[54,311],{"x":296,"y":146,"width":262,"height":263,"rx":69,"fill":111,"stroke":91,"style":139},[78,313,314],{"x":292,"y":285,"style":127,"fill":91,"textAnchor":84},"a project layer — crash",[154,316],{"x1":317,"y1":318,"x2":319,"y2":318,"stroke":76,"style":320},"320","120","434","stroke-width:2;marker-end:url(#ownArrow)",[78,322,325],{"x":323,"y":324,"style":106,"fill":76,"textAnchor":84},"377","112","cancel",[154,327],{"x1":319,"y1":254,"x2":317,"y2":254,"stroke":76,"style":320},[78,329,331],{"x":323,"y":330,"style":106,"fill":76,"textAnchor":84},"162","progress, result",[78,333,335],{"x":80,"y":334,"style":127,"fill":100,"textAnchor":84},"268","Take the snapshot on the main thread before the task starts",[172,337,339],{"id":338},"the-minimal-task","The minimal task",[341,342,347],"pre",{"className":343,"code":344,"language":345,"meta":346,"style":346},"language-python shiki shiki-themes github-dark","from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis\n\nclass CountTask(QgsTask):\n    def __init__(self, source):\n        super().__init__(\"Counting features\", QgsTask.CanCancel)\n        self.source = source\n        self.total = 0\n\n    def run(self):\n        for index, feature in enumerate(self.source.getFeatures()):\n            if self.isCanceled():\n                return False\n            self.total += 1\n            if index % 500 == 0:\n                self.setProgress(100 * index \u002F max(self.source.featureCount(), 1))\n        return True\n\n    def finished(self, result):\n        if result:\n            QgsMessageLog.logMessage(f\"counted {self.total}\", \"MyPlugin\", Qgis.Info)\n\ntask = CountTask(layer.materialize(QgsFeatureRequest()))\nQgsApplication.taskManager().addTask(task)\n","python","",[213,348,349,367,374,392,405,426,441,454,459,470,493,505,514,528,551,587,596,601,612,621,654,659,670],{"__ignoreMap":346},[350,351,353,357,361,364],"span",{"class":154,"line":352},1,[350,354,356],{"class":355},"snl16","from",[350,358,360],{"class":359},"s95oV"," qgis.core ",[350,362,363],{"class":355},"import",[350,365,366],{"class":359}," QgsTask, QgsApplication, QgsMessageLog, Qgis\n",[350,368,370],{"class":154,"line":369},2,[350,371,373],{"emptyLinePlaceholder":372},true,"\n",[350,375,377,380,384,387,389],{"class":154,"line":376},3,[350,378,379],{"class":355},"class",[350,381,383],{"class":382},"svObZ"," CountTask",[350,385,386],{"class":359},"(",[350,388,133],{"class":382},[350,390,391],{"class":359},"):\n",[350,393,395,398,402],{"class":154,"line":394},4,[350,396,397],{"class":355},"    def",[350,399,401],{"class":400},"sDLfK"," __init__",[350,403,404],{"class":359},"(self, source):\n",[350,406,408,411,414,417,419,423],{"class":154,"line":407},5,[350,409,410],{"class":400},"        super",[350,412,413],{"class":359},"().",[350,415,416],{"class":400},"__init__",[350,418,386],{"class":359},[350,420,422],{"class":421},"sU2Wk","\"Counting features\"",[350,424,425],{"class":359},", QgsTask.CanCancel)\n",[350,427,429,432,435,438],{"class":154,"line":428},6,[350,430,431],{"class":400},"        self",[350,433,434],{"class":359},".source ",[350,436,437],{"class":355},"=",[350,439,440],{"class":359}," source\n",[350,442,444,446,449,451],{"class":154,"line":443},7,[350,445,431],{"class":400},[350,447,448],{"class":359},".total ",[350,450,437],{"class":355},[350,452,453],{"class":400}," 0\n",[350,455,457],{"class":154,"line":456},8,[350,458,373],{"emptyLinePlaceholder":372},[350,460,462,464,467],{"class":154,"line":461},9,[350,463,397],{"class":355},[350,465,466],{"class":382}," run",[350,468,469],{"class":359},"(self):\n",[350,471,473,476,479,482,485,487,490],{"class":154,"line":472},10,[350,474,475],{"class":355},"        for",[350,477,478],{"class":359}," index, feature ",[350,480,481],{"class":355},"in",[350,483,484],{"class":400}," enumerate",[350,486,386],{"class":359},[350,488,489],{"class":400},"self",[350,491,492],{"class":359},".source.getFeatures()):\n",[350,494,496,499,502],{"class":154,"line":495},11,[350,497,498],{"class":355},"            if",[350,500,501],{"class":400}," self",[350,503,504],{"class":359},".isCanceled():\n",[350,506,508,511],{"class":154,"line":507},12,[350,509,510],{"class":355},"                return",[350,512,513],{"class":400}," False\n",[350,515,517,520,522,525],{"class":154,"line":516},13,[350,518,519],{"class":400},"            self",[350,521,448],{"class":359},[350,523,524],{"class":355},"+=",[350,526,527],{"class":400}," 1\n",[350,529,531,533,536,539,542,545,548],{"class":154,"line":530},14,[350,532,498],{"class":355},[350,534,535],{"class":359}," index ",[350,537,538],{"class":355},"%",[350,540,541],{"class":400}," 500",[350,543,544],{"class":355}," ==",[350,546,547],{"class":400}," 0",[350,549,550],{"class":359},":\n",[350,552,554,557,560,563,566,568,571,574,576,578,581,584],{"class":154,"line":553},15,[350,555,556],{"class":400},"                self",[350,558,559],{"class":359},".setProgress(",[350,561,562],{"class":400},"100",[350,564,565],{"class":355}," *",[350,567,535],{"class":359},[350,569,570],{"class":355},"\u002F",[350,572,573],{"class":400}," max",[350,575,386],{"class":359},[350,577,489],{"class":400},[350,579,580],{"class":359},".source.featureCount(), ",[350,582,583],{"class":400},"1",[350,585,586],{"class":359},"))\n",[350,588,590,593],{"class":154,"line":589},16,[350,591,592],{"class":355},"        return",[350,594,595],{"class":400}," True\n",[350,597,599],{"class":154,"line":598},17,[350,600,373],{"emptyLinePlaceholder":372},[350,602,604,606,609],{"class":154,"line":603},18,[350,605,397],{"class":355},[350,607,608],{"class":382}," finished",[350,610,611],{"class":359},"(self, result):\n",[350,613,615,618],{"class":154,"line":614},19,[350,616,617],{"class":355},"        if",[350,619,620],{"class":359}," result:\n",[350,622,624,627,630,633,636,639,642,645,648,651],{"class":154,"line":623},20,[350,625,626],{"class":359},"            QgsMessageLog.logMessage(",[350,628,629],{"class":355},"f",[350,631,632],{"class":421},"\"counted ",[350,634,635],{"class":400},"{self",[350,637,638],{"class":359},".total",[350,640,641],{"class":400},"}",[350,643,644],{"class":421},"\"",[350,646,647],{"class":359},", ",[350,649,650],{"class":421},"\"MyPlugin\"",[350,652,653],{"class":359},", Qgis.Info)\n",[350,655,657],{"class":154,"line":656},21,[350,658,373],{"emptyLinePlaceholder":372},[350,660,662,665,667],{"class":154,"line":661},22,[350,663,664],{"class":359},"task ",[350,666,437],{"class":355},[350,668,669],{"class":359}," CountTask(layer.materialize(QgsFeatureRequest()))\n",[350,671,673],{"class":154,"line":672},23,[350,674,675],{"class":359},"QgsApplication.taskManager().addTask(task)\n",[14,677,678,681,682,685,686,689,690,693,694,696,697,700,701,704],{},[206,679,680],{},"Breakdown:"," ",[213,683,684],{},"run()"," executes on a worker thread and may touch nothing owned by the main thread; ",[213,687,688],{},"finished()"," runs back on the main thread, which is where results are applied and messages shown. Returning ",[213,691,692],{},"False"," from ",[213,695,684],{}," — including after a cancellation — tells the manager the task did not complete. Checking ",[213,698,699],{},"isCanceled()"," inside the loop is what makes the cancel button work: without it the flag is set and ignored. ",[213,702,703],{},"layer.materialize()"," produces an independent in-memory copy the task can own safely. Finally, keeping a reference to the task matters more than it looks: the task manager takes ownership, but a task garbage-collected before it starts simply never runs — the single most common cause of \"my task does nothing\".",[14,706,707,708,710],{},"The full treatment, including tasks that depend on other tasks and how to return a layer to the project, is in ",[21,709,182],{"href":181},".",[172,712,714],{"id":713},"faster-beats-asynchronous","Faster beats asynchronous",[14,716,717],{},"Threading is a way to hide a slow operation. Making it fast is better, and in PyQGIS the same three causes account for most slowness.",[14,719,720,723,724,727,728,731],{},[206,721,722],{},"Reading attributes and geometry you do not need."," A ",[213,725,726],{},"QgsFeatureRequest"," that sets ",[213,729,730],{},"NoGeometry"," and a subset of attributes can cut iteration time by an order of magnitude on a wide table, because the provider stops serialising columns nobody reads.",[14,733,734,737,738,741,742,710],{},[206,735,736],{},"Doing spatial comparisons without an index."," Comparing every feature against every other feature is quadratic; a ",[213,739,740],{},"QgsSpatialIndex"," turns the inner loop into a candidate lookup, as shown in ",[21,743,745],{"href":744},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build a Spatial Index in PyQGIS",[14,747,748,751],{},[206,749,750],{},"Editing without a transaction boundary."," Committing per feature makes every write a round trip. Batch them, or use the provider directly.",[14,753,754],{},[39,755,758,761,764,767,770,774,780,787,793,797,802,806,810,813,818,822,827,831],{"viewBox":756,"role":42,"ariaLabel":757,"xmlns":44},"0 0 760 250","Bar comparison of iteration time for a full read, an attribute subset, no geometry, and both optimisations combined",[46,759,760],{},"What a feature request saves on a wide table",[50,762,763],{},"Four bars show relative time to iterate the same 200 000-feature table. Reading everything is the longest. Requesting only two attributes is noticeably shorter. Dropping geometry is shorter still. Doing both is a small fraction of the original.",[54,765],{"x":56,"y":56,"width":57,"height":766,"fill":59},"250",[78,768,769],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Ask for less and the provider does less work",[154,771],{"x1":772,"y1":96,"x2":772,"y2":773,"stroke":100,"style":101},"230","204",[78,775,779],{"x":776,"y":97,"style":777,"fill":76,"textAnchor":778},"222","text-anchor:end;font-size:11px;font-family:sans-serif","end","everything",[54,781],{"x":782,"y":783,"width":784,"height":785,"rx":98,"fill":91,"fillOpacity":786,"stroke":91,"style":139},"234","64","400","26",0.35,[78,788,792],{"x":789,"y":790,"style":791,"fill":76},"646","82","font-size:11px;font-family:sans-serif","1.00×",[78,794,796],{"x":776,"y":795,"style":777,"fill":76,"textAnchor":778},"118","2 attributes",[54,798],{"x":782,"y":799,"width":800,"height":785,"rx":98,"fill":801,"fillOpacity":786,"stroke":801,"style":139},"102","286","#b45309",[78,803,805],{"x":804,"y":318,"style":791,"fill":76},"532","0.61×",[78,807,809],{"x":776,"y":808,"style":777,"fill":76,"textAnchor":778},"156","no geometry",[54,811],{"x":782,"y":95,"width":812,"height":785,"rx":98,"fill":251,"fillOpacity":786,"stroke":251,"style":139},"197",[78,814,817],{"x":815,"y":816,"style":791,"fill":76},"443","158","0.42×",[78,819,821],{"x":776,"y":820,"style":777,"fill":76,"textAnchor":778},"194","both",[54,823],{"x":782,"y":824,"width":825,"height":785,"rx":98,"fill":132,"fillOpacity":826,"stroke":132,"style":139},"178","61",0.4,[78,828,830],{"x":829,"y":146,"style":791,"fill":132},"307","0.13×",[78,832,834],{"x":80,"y":833,"style":127,"fill":100,"textAnchor":84},"232","Relative iteration time over one 200 000-feature table with 40 columns — the ratio, not the seconds, is the point",[172,836,838],{"id":837},"edits-belong-in-batches","Edits belong in batches",[14,840,841,842,845],{},"The third common cause of a frozen plugin is not computation at all — it is committing. Every ",[213,843,844],{},"commitChanges()"," is a transaction, and a transaction per feature turns a thousand small writes into a thousand round trips.",[341,847,849],{"className":343,"code":848,"language":345,"meta":346,"style":346},"layer.startEditing()\nfor feature in layer.getFeatures():\n    layer.changeAttributeValue(feature.id(), field_index, compute(feature))\nlayer.commitChanges()          # once, at the end\n",[213,850,851,856,869,874],{"__ignoreMap":346},[350,852,853],{"class":154,"line":352},[350,854,855],{"class":359},"layer.startEditing()\n",[350,857,858,861,864,866],{"class":154,"line":369},[350,859,860],{"class":355},"for",[350,862,863],{"class":359}," feature ",[350,865,481],{"class":355},[350,867,868],{"class":359}," layer.getFeatures():\n",[350,870,871],{"class":154,"line":376},[350,872,873],{"class":359},"    layer.changeAttributeValue(feature.id(), field_index, compute(feature))\n",[350,875,876,879],{"class":154,"line":394},[350,877,878],{"class":359},"layer.commitChanges()          ",[350,880,882],{"class":881},"sjoCn","# once, at the end\n",[14,884,885,887,888,30,891,681,893,897,898,901,902,710],{},[206,886,680],{}," Opening the edit session once and committing once lets QGIS send the whole change set as a single transaction. The version of this loop with ",[213,889,890],{},"startEditing()",[213,892,844],{},[894,895,896],"em",{},"inside"," the loop is functionally identical and, on a PostGIS layer, roughly two orders of magnitude slower. For a bulk update with no need for undo, skip the edit buffer entirely and call ",[213,899,900],{},"dataProvider().changeAttributeValues()"," with a dictionary of changes — the approach used in ",[21,903,905],{"href":904},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in PyQGIS",[14,907,908,909,912],{},"The same principle governs the canvas. Each ",[213,910,911],{},"triggerRepaint()"," schedules a redraw, so calling it inside a loop over fifty layers asks for fifty renders of a canvas nobody has seen yet. Change everything, then repaint once.",[172,914,916],{"id":915},"caching-what-you-look-up-repeatedly","Caching what you look up repeatedly",[14,918,919],{},"A surprising share of plugin slowness is lookup, not work. Three of them recur often enough to be worth naming.",[14,921,922,681,925,928,929,932],{},[206,923,924],{},"Layer lookups.",[213,926,927],{},"QgsProject.instance().mapLayersByName(\"roads\")"," walks the project's layer registry and builds a list. Inside a per-feature loop that is thousands of walks; hoist it above the loop, or hold the layer id and use ",[213,930,931],{},"mapLayer(layer_id)",", which is a dictionary lookup.",[14,934,935,681,938,941,942,945],{},[206,936,937],{},"Field indexes.",[213,939,940],{},"feature[\"area_m2\"]"," resolves the name to an index on every access. In a tight loop, resolve it once with ",[213,943,944],{},"layer.fields().indexOf(\"area_m2\")"," and index by number.",[14,947,948,951,952,955],{},[206,949,950],{},"Coordinate transforms."," Constructing a ",[213,953,954],{},"QgsCoordinateTransform"," is expensive because it initialises PROJ machinery; reuse one object for the whole loop rather than building it per point.",[341,957,959],{"className":343,"code":958,"language":345,"meta":346,"style":346},"fields = layer.fields()\narea_index = fields.indexOf(\"area_m2\")\nref_index = fields.indexOf(\"parcel_ref\")\n\nfor feature in layer.getFeatures(request):\n    attributes = feature.attributes()\n    process(attributes[ref_index], attributes[area_index])\n",[213,960,961,971,987,1001,1005,1016,1026],{"__ignoreMap":346},[350,962,963,966,968],{"class":154,"line":352},[350,964,965],{"class":359},"fields ",[350,967,437],{"class":355},[350,969,970],{"class":359}," layer.fields()\n",[350,972,973,976,978,981,984],{"class":154,"line":369},[350,974,975],{"class":359},"area_index ",[350,977,437],{"class":355},[350,979,980],{"class":359}," fields.indexOf(",[350,982,983],{"class":421},"\"area_m2\"",[350,985,986],{"class":359},")\n",[350,988,989,992,994,996,999],{"class":154,"line":376},[350,990,991],{"class":359},"ref_index ",[350,993,437],{"class":355},[350,995,980],{"class":359},[350,997,998],{"class":421},"\"parcel_ref\"",[350,1000,986],{"class":359},[350,1002,1003],{"class":154,"line":394},[350,1004,373],{"emptyLinePlaceholder":372},[350,1006,1007,1009,1011,1013],{"class":154,"line":407},[350,1008,860],{"class":355},[350,1010,863],{"class":359},[350,1012,481],{"class":355},[350,1014,1015],{"class":359}," layer.getFeatures(request):\n",[350,1017,1018,1021,1023],{"class":154,"line":428},[350,1019,1020],{"class":359},"    attributes ",[350,1022,437],{"class":355},[350,1024,1025],{"class":359}," feature.attributes()\n",[350,1027,1028],{"class":154,"line":443},[350,1029,1030],{"class":359},"    process(attributes[ref_index], attributes[area_index])\n",[14,1032,1033,681,1035,1038,1039,1042,1043,1046,1047,1050],{},[206,1034,680],{},[213,1036,1037],{},"attributes()"," returns the row's values as a plain list once, after which access is a list index rather than a name resolution. Resolving the two indexes before the loop makes the intent explicit and survives a schema change more gracefully than hard-coded numbers, because ",[213,1040,1041],{},"indexOf()"," returns ",[213,1044,1045],{},"-1"," for a missing field — worth asserting on before the loop rather than discovering as a confusing ",[213,1048,1049],{},"IndexError"," on the first feature.",[172,1052,1054],{"id":1053},"measure-before-you-optimise","Measure before you optimise",[14,1056,1057,1058,1061,1062,1065,1066,1068],{},"Intuition about which line is slow is wrong often enough to be worth distrusting. ",[213,1059,1060],{},"cProfile"," works normally inside the QGIS Python console and inside a plugin, and a single run against real data usually shows that ninety percent of the time is in one call — frequently a geometry conversion inside a loop, or a ",[213,1063,1064],{},"mapLayersByName()"," lookup that should have happened once. ",[21,1067,197],{"href":196}," covers the mechanics and how to read the output.",[172,1070,1072],{"id":1071},"when-a-background-task-is-the-wrong-answer","When a background task is the wrong answer",[14,1074,1075],{},"Threading has a cost — in complexity, in the classes of bug it introduces, and in the reviewer's time. Three situations look like they need a task and do not.",[14,1077,1078,1081],{},[206,1079,1080],{},"Work under a second."," A progress bar that flashes and disappears is worse than no feedback at all. Below roughly one second, do the work inline and set a busy cursor.",[14,1083,1084,1087],{},[206,1085,1086],{},"Work the user must wait for anyway."," If nothing else can sensibly be done until the result arrives — the dialog's next step depends on it, and the map is meaningless until it updates — a modal operation with a progress dialog is honest and much simpler. Moving it to a task and then disabling the whole interface achieves the same thing with more code.",[14,1089,1090,1093],{},[206,1091,1092],{},"Work that is slow because it is wrong."," A per-feature loop doing a spatial comparison against another layer is not slow because it is on the main thread; it is slow because it is quadratic. Threading it hides a two-minute wait behind a progress bar that will become a twenty-minute wait when the data grows. Fix the algorithm.",[14,1095,1096],{},"The honest test is whether the user has something useful to do while the work runs. If they can keep panning the map, inspecting other layers or setting up the next operation, a task earns its complexity. If they will sit and watch the bar, it probably does not.",[172,1098,1100],{"id":1099},"testing-code-that-runs-on-a-task","Testing code that runs on a task",[14,1102,1103],{},"Task code is ordinary code that happens to be called from another thread, and it is testable if you keep the two separable.",[341,1105,1107],{"className":343,"code":1106,"language":345,"meta":346,"style":346},"def summarise_areas(source, is_cancelled=lambda: False, on_progress=lambda p: None):\n    \"\"\"Pure function: no widgets, no project, no QgsTask.\"\"\"\n    total = 0.0\n    count = source.featureCount() or 1\n    for index, feature in enumerate(source.getFeatures()):\n        if is_cancelled():\n            return None\n        total += feature.geometry().area()\n        if index % 500 == 0:\n            on_progress(100 * index \u002F count)\n    return total\n\n\nclass AreaSumTask(QgsTask):\n    def run(self):\n        self.result = summarise_areas(self.source, self.isCanceled, self.setProgress)\n        return self.result is not None\n",[213,1108,1109,1141,1146,1156,1171,1185,1192,1200,1210,1226,1242,1250,1254,1258,1271,1279,1306],{"__ignoreMap":346},[350,1110,1111,1114,1117,1120,1123,1126,1128,1131,1133,1136,1139],{"class":154,"line":352},[350,1112,1113],{"class":355},"def",[350,1115,1116],{"class":382}," summarise_areas",[350,1118,1119],{"class":359},"(source, is_cancelled",[350,1121,1122],{"class":355},"=lambda",[350,1124,1125],{"class":359},": ",[350,1127,692],{"class":400},[350,1129,1130],{"class":359},", on_progress",[350,1132,1122],{"class":355},[350,1134,1135],{"class":359}," p: ",[350,1137,1138],{"class":400},"None",[350,1140,391],{"class":359},[350,1142,1143],{"class":154,"line":369},[350,1144,1145],{"class":421},"    \"\"\"Pure function: no widgets, no project, no QgsTask.\"\"\"\n",[350,1147,1148,1151,1153],{"class":154,"line":376},[350,1149,1150],{"class":359},"    total ",[350,1152,437],{"class":355},[350,1154,1155],{"class":400}," 0.0\n",[350,1157,1158,1161,1163,1166,1169],{"class":154,"line":394},[350,1159,1160],{"class":359},"    count ",[350,1162,437],{"class":355},[350,1164,1165],{"class":359}," source.featureCount() ",[350,1167,1168],{"class":355},"or",[350,1170,527],{"class":400},[350,1172,1173,1176,1178,1180,1182],{"class":154,"line":407},[350,1174,1175],{"class":355},"    for",[350,1177,478],{"class":359},[350,1179,481],{"class":355},[350,1181,484],{"class":400},[350,1183,1184],{"class":359},"(source.getFeatures()):\n",[350,1186,1187,1189],{"class":154,"line":428},[350,1188,617],{"class":355},[350,1190,1191],{"class":359}," is_cancelled():\n",[350,1193,1194,1197],{"class":154,"line":443},[350,1195,1196],{"class":355},"            return",[350,1198,1199],{"class":400}," None\n",[350,1201,1202,1205,1207],{"class":154,"line":456},[350,1203,1204],{"class":359},"        total ",[350,1206,524],{"class":355},[350,1208,1209],{"class":359}," feature.geometry().area()\n",[350,1211,1212,1214,1216,1218,1220,1222,1224],{"class":154,"line":461},[350,1213,617],{"class":355},[350,1215,535],{"class":359},[350,1217,538],{"class":355},[350,1219,541],{"class":400},[350,1221,544],{"class":355},[350,1223,547],{"class":400},[350,1225,550],{"class":359},[350,1227,1228,1231,1233,1235,1237,1239],{"class":154,"line":472},[350,1229,1230],{"class":359},"            on_progress(",[350,1232,562],{"class":400},[350,1234,565],{"class":355},[350,1236,535],{"class":359},[350,1238,570],{"class":355},[350,1240,1241],{"class":359}," count)\n",[350,1243,1244,1247],{"class":154,"line":495},[350,1245,1246],{"class":355},"    return",[350,1248,1249],{"class":359}," total\n",[350,1251,1252],{"class":154,"line":507},[350,1253,373],{"emptyLinePlaceholder":372},[350,1255,1256],{"class":154,"line":516},[350,1257,373],{"emptyLinePlaceholder":372},[350,1259,1260,1262,1265,1267,1269],{"class":154,"line":530},[350,1261,379],{"class":355},[350,1263,1264],{"class":382}," AreaSumTask",[350,1266,386],{"class":359},[350,1268,133],{"class":382},[350,1270,391],{"class":359},[350,1272,1273,1275,1277],{"class":154,"line":553},[350,1274,397],{"class":355},[350,1276,466],{"class":382},[350,1278,469],{"class":359},[350,1280,1281,1283,1286,1288,1291,1293,1296,1298,1301,1303],{"class":154,"line":589},[350,1282,431],{"class":400},[350,1284,1285],{"class":359},".result ",[350,1287,437],{"class":355},[350,1289,1290],{"class":359}," summarise_areas(",[350,1292,489],{"class":400},[350,1294,1295],{"class":359},".source, ",[350,1297,489],{"class":400},[350,1299,1300],{"class":359},".isCanceled, ",[350,1302,489],{"class":400},[350,1304,1305],{"class":359},".setProgress)\n",[350,1307,1308,1310,1312,1314,1317,1320],{"class":154,"line":598},[350,1309,592],{"class":355},[350,1311,501],{"class":400},[350,1313,1285],{"class":359},[350,1315,1316],{"class":355},"is",[350,1318,1319],{"class":355}," not",[350,1321,1199],{"class":400},[14,1323,1324,1326,1327,1329,1330,1334,1335,1338],{},[206,1325,680],{}," The work lives in a function that knows nothing about QGIS's task machinery — it takes a feature source and two callables, and returns a number or ",[213,1328,1138],{},". That function can be tested directly against a memory layer with no application, no threading and no waiting, using the fixtures described in ",[21,1331,1333],{"href":1332},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","Unit Test a QGIS Plugin with pytest",". Cancellation is testable too: pass a callable that returns ",[213,1336,1337],{},"True"," after the third call and assert the function stops. The task subclass shrinks to plumbing, which is the part least worth testing and most worth keeping small.",[14,1340,1341],{},"The same separation pays off when the requirements change. Work written this way can be called inline from a script, from a Processing algorithm, or from a task, without being rewritten for each.",[172,1343,1345],{"id":1344},"ordering-several-tasks","Ordering several tasks",[14,1347,1348,1349,1351],{},"Real plugin work is often a sequence: download, process, then style and add to the project. Running those as three independent tasks is wrong — the second would start before the first has produced anything — and running them as one long ",[213,1350,684],{}," throws away the manager's ability to report each stage.",[14,1353,1354],{},"The task manager understands dependencies, so declare them rather than coordinating by hand:",[341,1356,1358],{"className":343,"code":1357,"language":345,"meta":346,"style":346},"from qgis.core import QgsApplication\n\ndownload = DownloadTask(\"Fetching survey data\")\nprocess = ProcessTask(\"Cleaning geometry\")\nprocess.addSubTask(download, [], QgsTask.ParentDependsOnSubTask)\n\nQgsApplication.taskManager().addTask(process)\nself.tasks = [download, process]\n",[213,1359,1360,1371,1375,1390,1405,1410,1414,1419],{"__ignoreMap":346},[350,1361,1362,1364,1366,1368],{"class":154,"line":352},[350,1363,356],{"class":355},[350,1365,360],{"class":359},[350,1367,363],{"class":355},[350,1369,1370],{"class":359}," QgsApplication\n",[350,1372,1373],{"class":154,"line":369},[350,1374,373],{"emptyLinePlaceholder":372},[350,1376,1377,1380,1382,1385,1388],{"class":154,"line":376},[350,1378,1379],{"class":359},"download ",[350,1381,437],{"class":355},[350,1383,1384],{"class":359}," DownloadTask(",[350,1386,1387],{"class":421},"\"Fetching survey data\"",[350,1389,986],{"class":359},[350,1391,1392,1395,1397,1400,1403],{"class":154,"line":394},[350,1393,1394],{"class":359},"process ",[350,1396,437],{"class":355},[350,1398,1399],{"class":359}," ProcessTask(",[350,1401,1402],{"class":421},"\"Cleaning geometry\"",[350,1404,986],{"class":359},[350,1406,1407],{"class":154,"line":407},[350,1408,1409],{"class":359},"process.addSubTask(download, [], QgsTask.ParentDependsOnSubTask)\n",[350,1411,1412],{"class":154,"line":428},[350,1413,373],{"emptyLinePlaceholder":372},[350,1415,1416],{"class":154,"line":443},[350,1417,1418],{"class":359},"QgsApplication.taskManager().addTask(process)\n",[350,1420,1421,1423,1426,1428],{"class":154,"line":456},[350,1422,489],{"class":400},[350,1424,1425],{"class":359},".tasks ",[350,1427,437],{"class":355},[350,1429,1430],{"class":359}," [download, process]\n",[14,1432,1433,681,1435,1438,1439,1442],{},[206,1434,680],{},[213,1436,1437],{},"addSubTask()"," with ",[213,1440,1441],{},"ParentDependsOnSubTask"," tells the manager that the parent must not start until the subtask has completed successfully — and, importantly, that a failed subtask cancels the parent rather than letting it run on missing input. Only the parent is added to the manager; the subtask comes with it. Progress reported by the parent covers the whole group, so the user sees one entry rather than three competing bars. Keeping a reference to both remains necessary for the same garbage-collection reason as a single task.",[14,1444,1445],{},"Where the stages are genuinely independent — three regions processed in parallel — add them as separate tasks with no dependency and let the manager run them concurrently up to its thread limit. A fan-out like that is the one case where threading gives a real speed-up rather than only responsiveness, because the work actually proceeds in parallel.",[14,1447,1448,1449,1452],{},"What the manager deliberately does not give you is a guarantee about ",[894,1450,1451],{},"which"," thread a task runs on, or that two tasks will not run simultaneously. Anything shared between tasks — a file being written, a layer being edited — needs to be owned by exactly one of them.",[172,1454,1456],{"id":1455},"key-takeaways","Key takeaways",[1458,1459,1460,1471,1477,1486,1495,1501,1513],"ul",{},[1461,1462,1463,1468,1469,710],"li",{},[206,1464,1465,1466,710],{},"Never touch widgets or project layers from ",[213,1467,684],{}," Prepare on the main thread, compute on the task thread, apply in ",[213,1470,688],{},[1461,1472,1473,1476],{},[206,1474,1475],{},"Keep a reference to the task"," you hand to the manager, or it may be collected before it starts.",[1461,1478,1479,1485],{},[206,1480,1481,1482,1484],{},"Check ",[213,1483,699],{}," inside every loop."," A cancel button that does not stop anything is worse than none.",[1461,1487,1488,1494],{},[206,1489,1490,1491,1493],{},"Try a narrower ",[213,1492,726],{}," first."," Dropping geometry and unused attributes often removes the need for threading entirely.",[1461,1496,1497,1500],{},[206,1498,1499],{},"Profile before optimising."," The slow line is rarely the one you would have guessed.",[1461,1502,1503,1506,1507,1509,1510,1512],{},[206,1504,1505],{},"Batch every edit."," One ",[213,1508,890],{}," and one ",[213,1511,844],{}," around the whole loop, never one per feature — the per-feature version is two orders of magnitude slower on a database layer.",[1461,1514,1515,1518],{},[206,1516,1517],{},"Report progress at a human rate"," — every few hundred features, not every one; the signal itself costs time.",[172,1520,1522],{"id":1521},"frequently-asked-questions","Frequently Asked Questions",[14,1524,1525,1528],{},[206,1526,1527],{},"How slow is slow enough to need a task?","\nRoughly a second of blocked interface is where users start to notice, and three seconds is where they start clicking again. Between one and three seconds a busy cursor is usually enough; beyond that, move the work off the main thread or make it faster.",[14,1530,1531,1534,1535,1537],{},[206,1532,1533],{},"Does a task make my plugin harder to test?","\nOnly if the work lives inside ",[213,1536,684],{},". Keep the computation in a plain function that takes a feature source and returns a value, and the tests never touch threading at all — the task subclass becomes a thin wrapper worth about four lines of coverage.",[14,1539,1540,1543,1544,1546],{},[206,1541,1542],{},"What happens to a running task when QGIS closes?","\nThe task manager asks each task to cancel and waits briefly. A task that checks ",[213,1545,699],{}," frequently exits cleanly; one that does not can delay shutdown or be terminated mid-write, which is another reason the check belongs in every loop.",[14,1548,1549,1552],{},[206,1550,1551],{},"Can I show results from a task that the user cancelled?","\nPartial results are usually misleading, and presenting them as if the operation completed is worse than discarding them. If partial output is genuinely useful — a long export that wrote most of its features — say so explicitly in the message rather than letting it look finished.",[14,1554,1555,1562,1563,1565],{},[206,1556,1557,1558,1561],{},"Can I use Python's ",[213,1559,1560],{},"threading"," module instead of QgsTask?","\nYou can start a thread, but you inherit every ownership rule with none of the integration: no entry in the task manager, no progress in the status bar, no cancellation, and no guarantee about which thread your callbacks run on. ",[213,1564,133],{}," exists precisely to make that correct.",[14,1567,1568,1571,1572,1574,1575,1578,1579,1581],{},[206,1569,1570],{},"Why does my task never run?","\nThe task object was garbage-collected. Store it on ",[213,1573,489],{},", or use ",[213,1576,1577],{},"QgsApplication.taskManager().addTask()"," and keep the reference until ",[213,1580,688],{}," fires.",[14,1583,1584,1587,1588,1591,1592,1594,1595,1598],{},[206,1585,1586],{},"Can a task run a Processing algorithm?","\nYes — ",[213,1589,1590],{},"processing.run()"," works inside ",[213,1593,684],{}," provided the inputs are file paths or layers the task owns. ",[213,1596,1597],{},"QgsProcessingAlgRunnerTask"," wraps this and handles the feedback plumbing for you.",[14,1600,1601,1604],{},[206,1602,1603],{},"How many tasks run at once?","\nThe task manager runs several concurrently, bounded by a thread count QGIS manages. Tasks that must run in order should declare dependencies rather than assume the manager's ordering.",[14,1606,1607,1610,1611,710],{},[206,1608,1609],{},"Is a background task useful in a script rather than a plugin?","\nRarely. A headless script has no interface to keep responsive, so a straight loop is simpler and easier to debug — see ",[21,1612,1614],{"href":1613},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation",[14,1616,1617,1620],{},[206,1618,1619],{},"Does moving work to a task make it faster?","\nNo. It makes the interface responsive while the work happens. Speed comes from doing less work, which is what feature requests, spatial indexes and batched edits are for.",[172,1622,1624],{"id":1623},"related-guides","Related Guides",[1458,1626,1627,1633,1637,1641,1647,1653,1657,1661,1665],{},[1461,1628,1629,1630,1632],{},"Up: ",[21,1631,24],{"href":23}," — the parent guide for this topic",[1461,1634,1635],{},[21,1636,29],{"href":28},[1461,1638,1639],{},[21,1640,34],{"href":33},[1461,1642,1643],{},[21,1644,1646],{"href":1645},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools and Canvas Interaction",[1461,1648,1649],{},[21,1650,1652],{"href":1651},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002F","Testing and CI for Plugins",[1461,1654,1655],{},[21,1656,182],{"href":181},[1461,1658,1659],{},[21,1660,187],{"href":186},[1461,1662,1663],{},[21,1664,192],{"href":191},[1461,1666,1667],{},[21,1668,197],{"href":196},[1670,1671,1672],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":346,"searchDepth":369,"depth":369,"links":1674},[1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687],{"id":174,"depth":369,"text":175},{"id":201,"depth":369,"text":202},{"id":338,"depth":369,"text":339},{"id":713,"depth":369,"text":714},{"id":837,"depth":369,"text":838},{"id":915,"depth":369,"text":916},{"id":1053,"depth":369,"text":1054},{"id":1071,"depth":369,"text":1072},{"id":1099,"depth":369,"text":1100},{"id":1344,"depth":369,"text":1345},{"id":1455,"depth":369,"text":1456},{"id":1521,"depth":369,"text":1522},{"id":1623,"depth":369,"text":1624},"Keep QGIS responsive while your plugin works — run jobs on QgsTask, report progress and cancellation, iterate features efficiently, and find the code that is actually slow.","md",{"slug":12,"type":1691,"breadcrumb":1692,"datePublished":1693,"dateModified":1693},"guide","Background Tasks & Performance","2026-08-10","\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance",{"title":5,"description":1688},"qgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Findex","alHtrve5y9U2BvftmrRAFF6peWDDJJoK3ebEtf2AjwQ",1786401337553]